我们先看一下纸牌游戏。一幅纸牌由 52 张不同的纸牌组成,发牌时必须产生不重复的纸牌,而且洗牌过程必须公平,即 52! 中纸牌顺序应该等概率出现。很明显这种随机排列所产生的随机数必须均匀分布且独立。由此代码如下:

  1. using System;
  2. using System.Diagnostics;
  3. namespace Lucifer.CSharp.Sample
  4. {
  5.     class Program
  6.     {
  7.         static void Main(string[] args)
  8.         {
  9.             //初始化牌局
  10.             int[] array = new int[52];
  11.             for (int i = 0; i < array.Length; i++)
  12.             {
  13.                 array = i;
  14.             }
  15.             //洗牌
  16.             Permute<int>(array);
  17.             //验证牌局
  18.             for (int i = 0; i < array.Length; i++)
  19.             {
  20.                 var value = array;
  21.                 for (int j = 0; j < array.Length; j++)
  22.                 {
  23.                     if (j == i) continue;
  24.                     Debug.Assert(array[j] != value);
  25.                 }
  26.             }
  27.         }
  28.         static void Permute<T>(T[] array)
  29.         {
  30.             Random random = new Random();
  31.             for (int i = 1; i < array.Length; i++)
  32.             {
  33.                 Swap<T>(array, i, random.Next(0, i));
  34.             }
  35.         }
  36.         static void Swap<T>(T[] array, int indexA, int indexB)
  37.         {
  38.             T temp = array[indexA];
  39.             array[indexA] = array[indexB];
  40.             array[indexB] = temp;
  41.         }
  42.     }
  43. }
复制代码

代码示例中的 Permute<T>(T[] array) 方法产生一个随机序列。第一个循环用 1, 2, 3, …, N 初始化该序列。第二个循环完成一次随机洗牌。在该循环的每次迭代中,我们讲 array[j] 的值于数组位置在区间[0, j)之间的某个元素相交换(也可能不交换)。

然而,我们要问的是 Permute<T>(T[] array) 方法产生的所有排列等概率吗?

若根据该算法,答案为是。因为一共有 N! 种可能的排列,而 Swap<T>(array, i, random.Next(0, i)); 这一句 N-1 次调用 Next 方法出现的不同结果也是 N! 种。但是,事实上答案为否,并非所有排列都是等概率。问题就出在可爱的伪随机数数生成器(Pseudo-Random Number Generator)上。PRNG 的随机性很大程度上限制了随机序列的随机性。所以,上述代码需要一个更好的伪随机数数生成器以使实际与理论更加相符。但是好的 PRNG 往往伴随着性能的下降,比如 Mt19937 随机化算法。