zoukankan      html  css  js  c++  java
  • [转]洗牌的学问

    来源: http://www.thecodeway.com/blog/?p=680

    几乎所有的程序员都写过类似于“洗牌”的算法,也就是将一个数组随机打乱后输出,虽然很简单,但是深入研究起来,这个小小的算法也是大有讲究。我在 面试程序员的时候,就会经常让他们当场写一个洗牌的函数,从中可以观察到他们对于这个问题的理解和写程序的基本功。
        在深入讨论之前,必须先定义出一个基本概念:究竟洗牌算法的本质是什么?也就是说,什么样的洗牌结果是“正确”的?
        云风曾经有一篇博文, 专门讨论了这个问题,他也给出了一个比较确切的定义,在经过洗牌函数后,如果能够保证每一个数据出现在所有位置的概率是相等的,那么这种算法是符合要求 的。在这个前提下,尽量降低时间复杂度和空间复杂度就能得到好的算法。

        第一个洗牌算法:

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

        大概是比较符合大脑对于洗牌的直观思维,这个算法经常出现在我遇到的面试结果中,虽然它符合我们对于洗牌算法的基本要求,但这个算法并不 好,首先它的复杂度为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: 这个概率是,推算过程可以参考这里,根据这个概率,需要交换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在实现上也是采取了第四种方法,看来还是那句 话,“不要重复制造轮子”

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

     以下内容摘自: http://www.matrix67.com/blog/archives/879

     假使你需要把一个数组随机打乱顺序进行重排。你需要保证重排后的结果是概率均等、完全随机的。下面两种算法哪一种是正确的?其 中,random(a,b)函数用于返回一个从a到b(包括a和b)的随机整数。

    1. for i:=1 to n do swap(a[i], a[random(1,n)]);
    2. for i:=1 to n do swap(a[i], a[random(i,n)]);


        如果不仔细思考的话,绝大多数人会认为第一个算法才是真正随机的,因为它的操作“更对称”,保证了概率均等。但静下心来仔细思考,你会发现第二种 算法才是真正满足随机性的。为了证明这一点,只需要注意到算法的本质是“随机确定a[1]的值,然后递归地对后n-1位进行操作”,用数学归纳法即可轻易 说明算法的正确性。而事实上,这段程序一共将会产生n*(n-1)*(n-2)*...*1种等可能的情况,它们正好与1至n的n!种排列一一对应。
         有人会问,那第一种算法为什么就错了呢?看它的样子多么对称美观啊……且慢,我还没说第一种算法是错的哦!虽然第一种算法将产生比第二种算法更多的可能 性,会导致一些重复的数列,但完全有可能每种数列重复了相同的次数,概率仍然是均等的。事实上,更有可能发生的是,这两种算法都是正确的,不过相比之下呢 第一种算法显得更加对称美观一些。为此,我们需要说明,第一种算法产生的所有情况均等地分成了n!个等价的结果。显然,这个算法将会产生n^n种情况,而 我们的排列一共有n!个,因此n^n必须能够被n!整除才行(否则就不能均等地分布了)。但是,n!里含有所有不超过n的质数,而n^n里却只有n的那几 个质因子。这表明要想n^n能被n!整除,n的质因子中必须含有所有不超过n的质数。这个结论看上去相当荒唐,反例遍地都是,并且直觉上告诉我们对于所有 大于2的n这都是不成立的。为了证明这一点,只需要注意到2是质数,并且根据Bertrand-Chebyshev定理,在n/2和n之间一定还有一个质 数。这两个质数的乘积已经大于n了。搞了半天,第一种看似对称而美观的算法居然是错的!

      
  • 相关阅读:
    influxdb + cadvisor + grafana 监控 docker容器应用性能
    zabbix_get命令
    【工作笔记】python+influxdb+grafana监控云行情
    沙雕与大婶 | 把5W2H融入你的架构设计吧
    GO系列 | 5分钟入门GO【译】
    Docker深入浅出系列 | 5分钟搭建你的私有镜像仓库
    Docker深入浅出系列 | Swarm多节点实战
    Docker如何给Springboot项目动态传参
    沙雕与大婶 | Mock掉你的外部依赖吧
    ed后缀读音规则
  • 原文地址:https://www.cnblogs.com/nsnow/p/1696654.html
Copyright © 2011-2022 走看看