zoukankan      html  css  js  c++  java
  • 洗牌算法汇总以及测试洗牌程序的正确性

    洗牌可以抽象为:给定一组排列,输出该排列的一个随机组合,本文代码中均以字符数组代表该排列

    算法1-算法3 都是在原序列的基础上进行交换,算法空间复杂度为O(1)

    算法1(错误):随机交换序列中的两张牌,交换n次(n为序列的长度),代码如下:

     1 void Shuffle_randomSwap(char *arr, const int len)
     2 {
     3     for(int i = 1; i <= len; i++)
     4     {
     5         int a = rand()%len;
     6         int b = rand()%len;
     7         char temp = arr[a];
     8         arr[a] = arr[b];
     9         arr[b] = temp;
    10     }
    11 }

    算法2(错误):遍历序列中的每个数,随机选择序列的某个数,把它和当前遍历到的数交换,代码如下:

     1 void Shuffle_FisherYates_change1(char *arr, const int len)
     2 {
     3     for(int i = len - 1; i >= 0; i--)
     4     {
     5         int a = rand()%len;
     6         int temp = arr[i];
     7         arr[i] = arr[a];
     8         arr[a] =  temp;
     9     }
    10 }

    算法3(正确):这是FisherYates洗牌算法,具体可参考wiki,算法的思想是每次从未选中的数字中随机挑选一个加入排列,时间复杂度为O(n),wiki上的伪代码如下

    To shuffle an array a of n elements (indices 0..n-1):
      for i from n − 1 downto 1 do
           j ← random integer with 0 ≤ ji
           exchange a[j] and a[i]
    代码实现:
     1 void Shuffle_FisherYates(char *arr, const int len)
     2 {
     3     for(int i = len - 1; i > 0; i--)
     4     {
     5         int a = rand()%(i + 1);
     6         int temp = arr[i];
     7         arr[i] = arr[a];
     8         arr[a] =  temp;
     9     }
    10 }

    下面我们来证明算法3的正确性,即证明每个数字在某个位置的概率相等,都为1/n:

    对于原排列最后一个数字:很显然他在第n个位置的概率是1/n,在倒数第二个位置概率是[(n-1)/n] * [1/(n-1)] = 1/n,在倒数第k个位置的概率是[(n-1)/n] * [(n-2)/(n-1)] *...* [(n-k+1)/(n-k+2)] *[1/(n-k+1)] = 1/n

    对于原排列的其他数字也可以同上求得他们在每个位置的概率都是1/n。

    这样算法2就是明显错误的:因为算法2中第一次随机选择后,第一个数字在第一个位置的概率是1/n,后面的随机选择只能使这个概率逐渐变小


    如果我们想保留原始的排列,洗牌后的排列放到一个额外的数组,那么改用怎么样的洗牌算法呢

    算法4(正确):inside-out算法,算法的思想就是遍历原数组,把原数组中位置 i 的数据随机放到新数组的前i个位置(包括第i个)中的某一个(假设放到第k个),然后把新数组的第k个位置的数放到新数组的第 i 个位置,代码如下:

     1 void Shuffle_InsideOut(char *arrSrc, const int len, char *arrDest)
     2 {
     3     arrDest[0] = arrSrc[0];
     4     for(int i = 1; i < len; i++)
     5     {
     6         int k = rand()%(i + 1);
     7         arrDest[i] = arrDest[k];
     8         arrDest[k] = arrSrc[i];
     9     }
    10 }

    该算法空间复杂度O(n),时间复杂度O(n)

    证明算法4的正确性:原数组的第 i 个元素在新数组的前 i 个位置的概率都是:(1/i) * [i/(i+1)] * [(i+1)/(i+2)] *...* [(n-1)/n] = 1/n,(即第i次刚好随机放到了该位置,在后面的n-i 次选择中该数字不被选中)

                               原数组的第 i 个元素在新数组的 i+1 (包括i + 1)以后的位置(假设是第k个位置)的概率是:(1/k) * [k/(k+1)] * [(k+1)/(k+2)] *...* [(n-1)/n] = 1/n(即第k次刚好随机放到了该位置,在后面的n-k次选择中该数字不被选中)

    算法4还可以用于未知原始数组大小的情况下的洗牌,从代码中可以看出,没加入一张新牌,后面的计算都和牌的总数目无关,只与当前牌的数目有关


    c++ STL中有随机洗牌的函数,头文件#include<algorithm>中,调用如下random_shuffle(arr, arr+len); (其中len是数组arr的元素个数),为了统一测试,我们测试该函数时使用如下调用:

    1 void Shuffle_STL(char *arr, const int len)
    2 {
    3     random_shuffle(arr, arr+len);
    4 }

    测试一个洗牌程序的正确性:运行该洗牌程序m次,然后计算每张牌在每个位置出现的次数,这个次数应该接近m/n,其中n为牌的数目

    测试算法1~3以及STL洗牌的函数:

     1 void testShuffle(char arr[], const int len, void(*shuffle)(char *, const int),
     2                  const int testTime)
     3 {
     4     int testResult[len][len];
     5     //testResult[i][j]表示牌arr[i]在第j个位置出现的次数
     6     char arrBackup[len];
     7     std::map<char, int> arrMap; //用于查找牌在原来数组中的位置
     8     for(int j = 0; j <len; j++)
     9     {
    10         arrMap.insert(map<char, int> :: value_type(arr[j], j));
    11         memset(testResult[j], 0, len*sizeof(int));
    12     }
    13 
    14     //对一副牌洗多次,统计每张牌在每个位置出现的次数
    15     for(int i = 1; i <= testTime; i++)
    16     {
    17         for(int j = 0; j <len; j++)
    18             arrBackup[j] = arr[j];
    19         shuffle(arrBackup, len);
    20         for(int j = 0; j <len; j++)
    21         {
    22             testResult[arrMap[arrBackup[j]]][j] ++;
    23         }
    24     }
    25     for(int i = 0; i < len; i++)
    26     {
    27         printf("%c:", arr[i]);
    28         for(int j = 0; j < len; j++)
    29         {
    30             printf("%7d",testResult[i][j]);
    31         }
    32         printf("
    ");
    33     }
    34     printf("----------------------------------
    ");
    35 }
    View Code

    测试算法4的函数:

     1 void testShuffle(char arr[], const int len,
     2                       void(*shuffle)(char *, const int, char *),
     3                       const int testTime)
     4 {
     5     int testResult[len][len];
     6     //testResult[i][j]表示牌arr[i]在第j个位置出现的次数
     7     std::map<char, int> arrMap; //用于查找牌在原来数组中的位置
     8     for(int j = 0; j <len; j++)
     9     {
    10         arrMap.insert(map<char, int> :: value_type(arr[j], j));
    11         memset(testResult[j], 0, len*sizeof(int));
    12     }
    13 
    14     //对一副牌洗多次,统计每张牌在每个位置出现的次数
    15     for(int i = 1; i <= testTime; i++)
    16     {
    17         char arrDest[len];
    18         shuffle(arr, len, arrDest);
    19         for(int j = 0; j <len; j++)
    20         {
    21             testResult[arrMap[arrDest[j]]][j] ++;
    22         }
    23     }
    24     for(int i = 0; i < len; i++)
    25     {
    26         printf("%c:", arr[i]);
    27         for(int j = 0; j < len; j++)
    28         {
    29             printf("%7d",testResult[i][j]);
    30         }
    31         printf("
    ");
    32     }
    33     printf("----------------------------------
    ");
    34 }
    View Code

    测试代码(每个算法测试100000次)

     1 int main()
     2 {
     3     srand((unsigned)time(NULL));
     4     char arr[10] = {'A','B','C','D','E','F','G','H','I','J'};
     5     printf("算法1:
    ");
     6     testShuffle(arr, 10, Shuffle_randomSwap, 100000);
     7     printf("算法2:
    ");
     8     testShuffle(arr, 10, Shuffle_FisherYates_change1, 100000);
     9     printf("算法3:
    ");
    10     testShuffle(arr, 10, Shuffle_FisherYates, 100000);
    11     printf("STL洗牌:
    ");
    12     testShuffle(arr, 10, Shuffle_STL, 100000);
    13     printf("算法4:
    ");
    14     testShuffle(arr, 10, Shuffle_InsideOut, 100000);
    15     return 0;
    16 }

    测试结果:

    算法1:主对角线上的次数明显是有问题的

    算法2:主对角线右上方第一个对角线(12798开头)数据明显有问题

     【版权声明】转载请注明出处:http://www.cnblogs.com/TenosDoIt/p/3384141.html

  • 相关阅读:
    spring cloud图形化dashboard是如何实现指标的收集展示的
    浮躁的我们
    c/c++学习系列之内存对齐
    c/c++学习系列之取整函数,数据宽度与对齐
    c/c++学习系列之memset()函数
    c/c++学习系列之putchar、getchar、puts、gets的运用
    c#学习系列之静态类,静态构造函数,静态成员,静态方法(总之各种静态)
    c#学习系列之字段(静态,常量,只读)
    C#中MessageBox用法大全(附效果图)<转>
    c#学习系列之Application.StartupPath的用法(美女时钟的做法)
  • 原文地址:https://www.cnblogs.com/TenosDoIt/p/3384141.html
Copyright © 2011-2022 走看看