假定随机数生成器可视为“真随机”的话,我们随机洗乱一副牌,非常有趣的是一些很自然的思路,并不一定能够产生均匀分布。假定牌面存储在一个数组里。
思路一:
每次生成2个随机数索引,交换在这2个索引位置上的牌。伪码:
static void ShufflePukes(int[] plist) { for (int i = 0; i < plist.Length; i++) { int ia = rand() % plist.length; int ib = rand() % plist.length; swap(plist[ia], plist[ib]); } }
用到了2次rand(),似乎是不必的。如果每次与第一个位置交换,直觉上是等价的。而且如果rand()是伪随机数列的话(几乎肯定是),相邻数之间关联性是很密切的,即伪随机数列按组分割的话不能看做是独立的,也即不是均匀分布的,印象中一本随机过程的书有提到,我只记得结论了。
于是我们去掉多余的一个rand()。
思路二:
static void ShufflePukes(int[] plist) { for (int i = 0; i < plist.Length; i++) { int ia = rand() % plist.length; swap(plist[0], plist[ia]); } }
看起来很漂亮。但实际上,无论思路一或思路二,牌面组合都不是均匀分布的。
正确的算法叫做Fisher_Yates算法。
每次产生随机索引后,分别与第一位,第二位,第三位...的位置进行交换。伪码如下(为方便我从后往前索引):
思路三:
static void ShufflePukes(int[] plist) { for (int i = plist.Length - 1; i >= 0; i--) { int ia = rand() % (i + 1); swap(plist[i], plist[ia]); } }
为何思路三是正确的?其差别相当细微,就是已经交换过的牌不再参与交换。
这可以从排列组合来考虑。n张不同的牌其排列组合共有n!种组合。很自然的一种算法如果产生的组合不等于n!,自然就是不均匀的。
对于思路三,其随机索引每次都不计入之前的位置,其组合为n!,所以是正确的。
对于思路二,每次迭代,若除去自身,每次都有n-1种可能,其组合上界为(n-1)^n,必定有一些牌被抽中的特别多,所以是错误的。