洗牌算法

转载 2011年05月25日 15:47:00

几乎所有的程序员都写过类似于“洗牌”的算法,也就是将一个数组随机打乱后输出,虽然很简单,但是深入研究起来,这个小小的算法也是大有讲究。我在面试程序员的时候,就会经常让他们当场写一个洗牌的函数,从中可以观察到他们对于这个问题的理解和写程序的基本功。

    在深入讨论之前,必须先定义出一个基本概念:究竟洗牌算法的本质是什么?也就是说,什么样的洗牌结果是“正确”的?

    云风曾经有一篇博文,专门讨论了这个问题,他也给出了一个比较确切的定义,在经过洗牌函数后,如果能够保证每一个数据出现在所有位置的概率是相等的,那么这种算法是符合要求的。在这个前提下,尽量降低时间复杂度和空间复杂度就能得到好的算法。

    第一个洗牌算法:

随机抽出一张牌,检查这张牌是否被抽取过,如果已经被抽取过,则重新抽取,直到找到没被抽出过的牌,然后把这张牌放入洗好的队列中,重复该过程,直到所有的牌被抽出。

    大概是比较符合大脑对于洗牌的直观思维,这个算法经常出现在我遇到的面试结果中,虽然它符合我们对于洗牌算法的基本要求,但这个算法并不好,首先它的复杂度为O(N2),而且需要额外的内存空间保存已经被抽出的牌的索引。所以当数据量比较大时,会极大降低效率。

    第二个算法:

设牌的张数为n,首先准备n个不容易碰撞的随机数,然后进行排序,通过排序可以得到一个打乱次序的序列,按照这个序列将牌打乱。

这也是一个符合要求的算法,但是同样需要额外的存储空间,在复杂度上也会取决于所采用的排序算法,所以仍然不是一个好的算法。

    第三个算法:

每次随机抽出两张牌交换,重复交换一定次数次后结束

void shuffle(int* data, int length)

{

    for(int i=0; i<SWAP_COUNTS; i++)

    {

        //Rand(min, max)返回[min, max)区间内的随机数

        int index1 = Rand(0, length);

        int index2 = Rand(0, length);

        std::swap(data[index1], data[index2]);

    }

}

    这又是一个常见的洗牌方法,比较有意思的问题是其中的“交换次数”,我们该如何确定一个合适的交换次数?简单的计算,交换m次后,具体某张牌始终没有被抽到的概率为((n-2)/n)^m,如果我们要求这个概率小于1/1000,那么 m>-3*ln(10)/ln(1-2/n),对于52张牌,这个数大约是176次,需要注意的是,这是满足“具体某张牌”始终没有被抽到的概率,如果需要满足“任意一张牌”没被抽到的概率小于1/1000,需要的次数还要大一些,但这个概率计算起来比较复杂,有兴趣的朋友可以试一下。

    Update: 这个概率是洗牌算法的学问 - 22andy22 - 22andy22的博客,推算过程可以参考这里,根据这个概率,需要交换280次才能符合要求

    第四个算法:

从第一张牌开始,将每张牌和随机的一张牌进行交换

void shuffle(int* data, int length)

{

    for(int i=0; i<length; i++)

    {

        int index = Rand(0, length);

        std::swap(data[i], data[index]);

     }

}

    很明显,这个算法是符合我们先前的要求的,时间复杂度为O(N),而且也不需要额外的临时空间,似乎我们找到了最优的算法,然而事实并非如此,看下一个算法。

    第五个算法:

void shuffle(int* data, int length)

{

    for(int i=1; i<length; i++)

    {

        int index = Rand(0, i);

        std::swap(data[i], data[index]);

     }

}

    一个有意思的情况出现了,这个算法和第三种算法非常相似,从直觉来说,似乎使数据“杂乱”的能力还要弱于第三种,但事实上,这种算法要强于第三种。要想严格的证明这一点并不容易,需要一些数学功底,有兴趣的朋友可以参照一下这篇论文,或者matrix67大牛的博文,也可以这样简单理解一下,对于n张牌的数据,实际排列的可能情况为n! 种,但第四种算法能够产生n^n种排列,远远大于实际的排列情况,而且n^n不能被n!整除,所以经过算法四所定义的牌与牌之间的交换程序,很可能一张牌被换来换去又被换回到原来的位置,所以这个算法不是最优的。而算法五输出的可能组合恰好是n!种,所以这个算法才是完美的。

    事情并没有结束,如果真的要找一个最优的算法,还是请出最终的冠军吧!

    第六个算法:

void shuffle(int* data, int length)

{

    std::random_shuffle(data, data+length);

}

    没错,用c++的标准库函数才是最优方案,事实上,std::random_shuffle在实现上也是采取了第四种方法,看来还是那句话,“不要重复制造轮子”

[经典面试题]完美洗牌算法

题目有个长度为2n的数组{a1,a2,a3,…,an,b1,b2,b3,…,bn},希望排序后{a1,b1,a2,b2,….,an,bn},请考虑有无时间复杂度o(n),空间复杂度0(1)的解法。来源...
  • SunnyYoona
  • SunnyYoona
  • 2015年02月13日 16:08
  • 7851

【算法详解】洗牌算法

1. 问题描述
  • robinjwong
  • robinjwong
  • 2014年01月14日 13:44
  • 5380

【每日算法】洗牌算法

洗牌算法 给定一个n个数的序列,设计一个算法将其随机打乱,保证每个数出现在任意一个位置的概率相同(也就是说在n!个的排列中,每一个排列出现的概率相同)。...
  • jiange_zh
  • jiange_zh
  • 2016年02月18日 10:27
  • 1528

[腾讯面试]洗牌算法

腾讯面试问题:如何对10首音乐随机播放? 知识点:洗牌算法。 C++版: #include #include using namespace std; int rand(int range...
  • hcy0727
  • hcy0727
  • 2012年05月18日 22:02
  • 2326

关于洗牌算法的一点总结

之前写斗地主的时候简单写了一个洗牌函数,基本思路是先产生一个顺序数组,遍历数组,每次产生一个(1~n)随机数,把这个随机数作为下标取出数组里的数与当前位置的数交换。 当时也没多想,反正能打乱数组顺序...
  • huang1433
  • huang1433
  • 2015年11月08日 10:31
  • 2004

麻将洗牌算法系列(1)

1,首先定义数组paiqiang[136](就是所有的牌)。 2,设置随机种子 3,这样就把136张牌洗乱了 for(i=0;i {  r=rand()%(136-i);  t=paiqiang[13...
  • dadalan
  • dadalan
  • 2009年08月16日 22:00
  • 1378

游戏中常见的洗牌算法

今天把游戏中用到的洗牌算法好好看了看,把自己感觉最易懂的和最经典的一种分享出来,当然,我这个是看过别人之后自己写的。 我们以扑克牌当做例子,一共有54张,如何一次就把排序全部打乱呢? 1、最笨的洗牌算...
  • jiasenhao_vip
  • jiasenhao_vip
  • 2016年01月25日 16:53
  • 1983

一个高效的洗牌算法分析

0   public static void randomDeliverCard(int length) {1         int[] card1 = new int[length];      ...
  • del_del
  • del_del
  • 2006年08月23日 10:04
  • 833

证明洗牌算法的随机性

证明洗牌算法的随机性 问题背景: 有一副牌假设有N张,请设计一个随机洗牌算法。 解决方案: 这里只给出一个可以使用数学证明每张牌出现在任何位置概率为1/N的算法。 Pok...
  • grdgrdgrd
  • grdgrdgrd
  • 2015年05月25日 14:03
  • 1252

用java实现三个经典的洗牌算法

三个经典的洗牌算法分别为Fisher-Yates算法,
  • georas
  • georas
  • 2014年07月09日 11:27
  • 1900
收藏助手
不良信息举报
您举报文章:洗牌算法
举报原因:
原因补充:

(最多只允许输入30个字)