饭后温柔

汉堡与老干妈同嚼 有可乐味
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

洗牌算法

Posted on 2016-06-25 22:09  饭后温柔  阅读(484)  评论(0编辑  收藏  举报

假定随机数生成器可视为“真随机”的话,我们随机洗乱一副牌,非常有趣的是一些很自然的思路,并不一定能够产生均匀分布。假定牌面存储在一个数组里。

 思路一:

  每次生成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,必定有一些牌被抽中的特别多,所以是错误的。