洗牌算法之Knuth Shuffle

洗牌这种技术活,我相信大家都有玩过,虽然手法各不相同,但是目的大部分应该是一样的,就是把牌的顺序打乱,创造一个公平的玩牌氛围。


洗牌算法(Shuffling Algorithm),顾名思义,它的产生是用来解决类似洗牌这种场景的问题的,目的是产生一串等概率的随机列,使得很难去预测牌的顺序。现在的各种牌类游戏都有自己的洗牌算法,为了保证游戏的趣味性,各自的实现中都有自己考虑的因素添加在其中。今天与大家分享的是名为Knuth Shuffle的洗牌算法,在TAOCP中可以找到,但是呢,这个算法其实不是高爷爷的原创,它的自然语言版本是在1938年由R.A. Fisher and F. Yates发表的,而程序语言的版本是1964年由R. Durstenfeld提出的。


算法描述:

Let X1, X2…. XN be the set of N numbers to be shuffled.

  1. Set j to N
  2. Generate a random number R. (uniformly distributed between 0 and 1)
  3. Set k to (jR+1). k is now a random integer, between 1 and j.
  4. Exchange Xk and Xj
  5. Decrease j by 1.
  6. If j > 1, return to step 2.

算法实现:

void KnuthShuffle(int *array, int len) {
    int rand;
    for (int i = len; i >=0; i--) {
        rand = GenRand(0, i);
        swap(array[i], array[rand]);
    }
}

GenRand(int min, int max)是一个随机数生成器。

这个算法的时间复杂度是O(n)级别的,可以说是很完美的算法了,那么为什么还有那么多别的洗牌算法存在呢?

这个算法从第N个数开始,每次从当前位置数字的前面随机抽取一个数字和它交换位置,循环只执行一遍,每个数字都不会出现在它原来的位置上,这样的序列有多少种呢?我们来计算一下。

设n个数都不在原来位置的种类为f(n),则n!=C(0)(n)*f(n) + C(1)(n)*f(n-1) + ... + C(n)(n)*f(0);其中显而易见,f(0)=1;f(1)=0;f(2)=1;f(3)=2;f(4)=9;等等。

所以这个f(n)是必然小于n!的,也就是说这个算法不是等概率的。

如何改动这个算法使之变成等概率的呢?最简单的改动是这样的:

void Shuffle(int *array, int len) {
    int rand;
    for (int i = len; i >=0; i--) {
        rand = GenRand(0, len);    //变动在这里len
        swap(array[i], array[rand]);
    }
}

但是那些大神们为什么没有这么做呢?原因是这个算法,在规模超过18时,恒等排列(identity permutation)是最有可能出现的,证明详见这里。

暂歇笔于2012.10.09

你可能感兴趣的:(编程之美)