洗牌算法-Fisher–Yates算法为什么好?

最近因为要做一个程序用到洗牌,就去研究了一下洗牌算法。
问题很简单,有一副扑克牌,执行一个算法,打乱他。
简单的问题往往隐藏了重要信息,比如这里的,打乱,什么才叫乱?
首先,设定我们已经拥有了系统提供的random函数,能够提供一个给定范围内的随机数,而且我们假定这个随机过程满足给定范围内的均匀分布。
然后,程序不妨效仿手洗法:
简单粗暴的手洗法是,中间抽一叠,再插回去,重复若干次。
更高级的是电影里面那种,用一些神乎其技的手法,把牌分成两部分,让A部分的每一张牌随机插入B部分的任意两张牌之间。
换言之,以上的方法可以归结为:分开再随机插回,或者简称随机对调。循环多次
那么,这样真的就足够乱吗?
现实的牌,在玩过以后一般会回收继续玩,也就是说,每一次洗的牌是在原本就近乎无序的基础上,再次洗牌,可以认为是伪随机的二次洗牌。
而电脑中的牌游戏,每开局一次,一般都是从deck中new一副新的来用,而deck在编程的时候为了方便,一般是按照花色和点数从小到大整整齐齐的记录下来的——这样方便确认deck的完整性。那么,在这种情况下,每一次洗牌其实是从1-52这种“新牌”手里洗牌。
玩过扑克牌都知道,一副新牌刚拆开,第一局往往存在洗不开的问题,一般都是试玩几局以后才感觉比较“乱”。
而程序一般来说,除非扑克牌有回收机制,比如三国杀中牌组打完了就会回收,然而,在游戏开始的时候,三国杀和大部分的扑克牌游戏,都是不存在回收这一机制的。
(*)当然,我也是简单猜测这些牌类游戏大公司的代码是从简考虑的,不排除存在那种公司为了随机性,事先在数据库中吃饱了没事干做了一大堆洗了多次的牌,玩家开局采用随机调用的方法直接get这种洗好了的牌,另算。
也就是说,用乱序对换模拟现实的洗牌,其“乱”的程度,直观上是不如的。毕竟每一次开局都在洗新牌。
当然,deck到牌堆不用new直接拖过来,而是用random的方法,其实也等价于先new,再random自排序。
那么,到底应该怎么评价乱呢?直观的评价虽然给人感觉上好理解,但是不好作为严格参考。
一个比较靠谱的依据是:概率论。
问题:一副扑克牌有n张,请问其序列能有多少种可能性?每种可能性等价吗?
答案:有n!中可能性,每种可能性等价(每个事件都是独立同分布的,证明略)。
那么,计算机洗牌算法的一种依据就是,洗出来的牌,在概率空间上,要充满这个n!样本空间才算起码的达标,无法充满完全样本空间的洗牌,就会【存在洗不出的序列】,不合格。之前的仿手洗随机插入法最大的设计问题在于次数的,对于真实扑克牌,正常人手洗一般也不会洗50多次吧。。。但是,反过来,试问,一副全新的牌,随机调换任意两张牌(以下把这个过程简称:洗),最少洗多少次以后,就能等价于到这个n!空间呢?
(洗:交换牌组中的当前顺序第x和第y张牌,其中x可以等于y。x和y均为1~n之间的随机整数。)
答案就是牌的张数。
n张牌组成的全新牌堆,洗1次,会得到n种可能的结果;
洗2次,会得到n*(n-1)种结果(减一是因为当第一次和第二次为互逆过程,等于回到最开始,没有新的排列出现);
洗3次,会得到n*(n-1)*(n-2)种结果;
洗...;
洗洗更健康...;
洗n次,会得到n!种结果,此时,已经完全充满n!的空间,洗更多次,样本空间不扩充。
所以,到这里,可以知道,对于一副新牌,最少只要随机的交换n次,才能在概率的意义上,让洗牌达到足够的“乱”,那么现在问题来了,如何选择一个好的算法?
一个比较容易想到的,简单粗暴的方法是,按照上面的文字,写出这样的伪代码:
 
  
...{
    for(int i=0;i<suit.length;i++)
    {
        random1 = Random.next(1,n);
        random2 = Random.next(1,n);
        exchange(suit[random1],suit[random2]);
    }
}...
也就是上面说过的
交换牌组中的当前顺序第x和第y张牌,其中x可以等于y,x和y均为1~n之间的随机整数。
这个算法在设计上能够充满整个样本空间,确实存在n!种可能性,但是不够好。
为什么不够好呢,因为这种算法不能够【确保】照顾到每一张牌。
随机洗牌的随机在于其不确定性,对于n张牌组成的有序排列,经过了n次随机选择,漏掉1只牌从未选过的概率不等于0,而且,随着牌的张数数量增加,这个概率非常可观。
现在就是经典的Fisher–Yates算法登场的时候了。下面给出伪代码:
 
  
...{
    for(int i=suit.length-1;i>0;i--)
    {
        random1 = Random.next(1,i);
        exchange(suit[random1],suit[i]);
    }
}...
这个算法和之前的算法最大的不同在于,每一次抽卡的范围在慢慢变小,具体的步骤可以看wiki给出的例子,我就直接照搬过来,源链接:
Fisher–Yates shuffle



洗牌算法-Fisher–Yates算法为什么好?_第1张图片

这个算法在样本空间上,跟前面简单粗暴的随机抽取一样。充满了n!的样本空间,好在哪里呢?因为它利用了抽卡本身的顺序,【保证照顾】到了每一张原本序列中的卡,而简单粗暴随机抽取存在出现重复位置的可能性,就等于浪费了一次排序的机会,换句话说,其等效抽卡次数因为出现了过去相同的洗法,有效洗牌次数下降,样本空间缩小,无法充满整个n!空间,所以有效性会下降。
而Fisher–Yates算法在原理上保证了不会出现浪费次数,重复选择的情况,导致样本空间一直保持n!,没有坍缩,这就是其在数学意义上优秀的原因。

你可能感兴趣的:(算法)