随机洗牌算法

前两天被问到一个随机洗牌的问题,当时脑子里想这不很简单么,生成随机数列嘛。回来之后自己动手实现了出来,然后也测试了一番,也上网搜索了一下关于随机洗牌的算法,发现现实好像不是那么简单,有一个地方容易形成误区,所以把这些记录下来。

这个误区已经有人很专业地分析过了,如下方:http://www.matrix67.com/blog/archives/879

    记得当年搞NOIp时,我犯过一个相当严重的错误:错误地把Floyd算法的i, j, k三层循环的位置顺序搞颠倒了。直到准备省选时我才突然意识到,Floyd算法应该最先枚举用于松驰操作的那个“中间变量”k,表示只经过从1到k的顶点的最短路;而我却一直习惯性地以为i, j, k应该顺次枚举。令人惊讶的是,这个错误跟了我那么久我居然从来都没有注意到过。后来,我发现有我这种经历的人不止一个。惯性思维很可能会让你接受一些明显错误的算法,并且让你用得坦坦荡荡,一辈子也发觉不了。
    假使你需要把一个数组随机打乱顺序进行重排。你需要保证重排后的结果是概率均等、完全随机的。下面两种算法哪一种是正确的?其中,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了。搞了半天,第一种看似对称而美观的算法居然是错的!

我先写了一个测试程序

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 using System.Collections;
  7 using System.IO;
  8 
  9 namespace Learning.useful.tool
 10 {
 11     class testWashCard
 12     {
 13         private ArrayList appearTimes = new ArrayList();
 14         private int cardCount;
 15         public int CardCount
 16         {
 17             get { return cardCount; }
 18             set { cardCount = value; }
 19         }
 20         private washCards algorithm;        // 洗牌算法接口 [9/9/2013 Administrator]
 21         public Learning.useful.tool.washCards Algorithm
 22         {
 23             get { return algorithm; }
 24             set { algorithm = value; }
 25         }
 26         private int testTimes;
 27         public int TestTimes
 28         {
 29             get { return testTimes; }
 30             set { testTimes = value; }
 31         }
 32         public testWashCard( int cardsCount = 10, int testTimes = 1000 )
 33         {
 34             CardCount = cardsCount;
 35             TestTimes = testTimes;
 36         }
 37         
 38         public void runTest()
 39         {
 40             if (algorithm == null)
 41             {
 42                 throw new Exception("algorithm haven't been set yet!");
 43             }
 44             appearTimes.Clear();
 45             for (int i = 0; i < CardCount * CardCount; ++i )
 46             {
 47                 appearTimes.Add(0);
 48             }
 49             FileStream fs = new FileStream("record.txt", FileMode.Create);
 50             StreamWriter sw = new StreamWriter(fs);
 51             StringBuilder sb = new StringBuilder();
 52             for (int i = 0; i < testTimes; ++i )
 53             {
 54                 System.Threading.Thread.Sleep(1);
 55                 long tick = DateTime.Now.Ticks;
 56                 Random ran = new Random((int)(tick & 0xffffffffL) | (int)(tick >> 32));
 57                 ArrayList result = algorithm.washCards(CardCount, ran);         //随机洗牌测试
 58                 for (int cardIndex = 0; cardIndex < CardCount; ++cardIndex )
 59                 {
 60                     int listIndex = cardIndex * CardCount + (int)result[cardIndex];
 61                     appearTimes[listIndex] = (int)appearTimes[listIndex] + 1;            //统计每张牌的随机位置
 62                 }
 63                 sb.Clear();
 64                 for (int index = 0; index < result.Count; ++index )
 65                 {
 66                     sb.AppendFormat("{0}\t", result[index]);
 67                 }
 68                 sw.WriteLine(sb.ToString());            //把过程记录到record.txt文件中
 69             }
 70             sw.Flush();
 71             sw.Close();
 72             fs.Close();
 73             showResult();
 74         }
 75         private void showResult()
 76         {
 77             StringBuilder sb = new StringBuilder();
 78             for (int i = 0; i < CardCount; ++i )
 79             {
 80                 sb.AppendFormat("{0}\t", i+1);
 81             }
 82             Console.WriteLine(sb.ToString());
 83             sb.Clear();
 84             double average = 0;
 85             FileStream fs = new FileStream("result.txt", FileMode.Create);
 86             StreamWriter sw = new StreamWriter(fs);
 87             for (int x = 0, y = 0; x * CardCount + y < appearTimes.Count; ++x)
 88             {
 89                 sb.AppendFormat("{0}\t", appearTimes[x * CardCount + y]);
 90                 average += (int)appearTimes[x * CardCount + y];
 91                 if (x == CardCount - 1)
 92                 {
 93                     x = -1;
 94                     y++;
 95                     Console.WriteLine(sb.ToString());
 96                     sw.WriteLine(sb.ToString());            //把测试结果记录到result.txt文件中去
 97                     sb.Clear();
 98                 }
 99             }
100             sw.Flush();
101             sw.Close();
102             fs.Close();
103             average /= appearTimes.Count;
104             double s = 0;
105             double deviation = 0;
106             for (int i = 0; i < appearTimes.Count; ++i)
107             {
108                 s += Math.Pow(average - (int)appearTimes[i], 2);
109                 double tmp = ((int)appearTimes[i] - average);
110                 deviation = tmp > deviation ? tmp : deviation;
111             }
112             s /= appearTimes.Count;
113             Console.WriteLine("方差是:{0}", s);
114             Console.WriteLine("最大误差是:{0}", deviation / average);
115         }
116     }
117 }

最开始是想通过计算方差来判断测试的结果,方差是衡量离散程度的,但是用于衡量这个测试结果不太适用。后来加上了最大误差的判断,就适合多了。

再贴上我自己的错误算法

 1 using System;
 2 using System.Collections;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace Learning.useful.tool
 9 {
10     class MyWashCardAlgorithm : washCards
11     {
12 
13         private ArrayList randomValue = new ArrayList();
14         private ArrayList result = new ArrayList();
15         public ArrayList washCards(int cardCount, Random ran)
16         {
17             randomValue.Clear();
18             result.Clear();
19             
20             for (int i = 0; i < cardCount; ++i )
21             {
22                 randomValue.Add(ran.Next(0, cardCount));    // 问题就出在这里,ran的范围
23                 result.Add(i);
24             }
25             sortRandomValue();
26             return result;
27         }
28         private void sortRandomValue()
29         {
30             /// <summary>
31             ///  快速排序
32             /// </summary>
33             /// 
34             quickSort(0, result.Count-1);
35         }
36 
37         private void quickSort(int head, int tail)
38         {
39             if (head >= tail)
40             {
41                 return;
42             }
43             int mid = (int)randomValue[head];
44             int midIndex = (int)result[head];
45             int p = head;
46             int q = head + 1;
47             for (; q <= tail; q++)
48             {
49                 if ((int)randomValue[q] < mid)
50                 {
51                     randomValue[p] = randomValue[q];
52                     result[p] = result[q];
53                     p++;
54                     randomValue[q] = randomValue[p];
55                     result[q] = result[p];
56                 }
57             }
58             randomValue[p] = mid;
59             result[p] = midIndex;
60             quickSort(head, p - 1);
61             quickSort(p + 1, tail);
62         }
63     }
64 }

以及正确的算法:

 1 using System;
 2 using System.Collections;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace Learning.useful.tool
 9 {
10     class coolShellWashCardAlgorithm : washCards
11     {
12 
13         private ArrayList randomValue = new ArrayList();
14         private ArrayList result = new ArrayList();
15 
16         public ArrayList washCards(int cardCount, Random ran)
17         {
18             randomValue.Clear();
19             result.Clear();
20             for (int i = 0; i < cardCount; ++i )
21             {
22                 result.Add(i);
23             }
24             ShuffleArray_Fisher_Yates(result, ran);
25             return result;
26         }
27 
28         private void ShuffleArray_Fisher_Yates(ArrayList arr, Random ran)
29         {
30             int i = arr.Count, j;
31             int temp;
32 
33             if (i == 0) return;
34             while (--i > 0)
35             {
36                 j = ran.Next(i + 1);
37                 temp = (int)arr[i];
38                 arr[i] = arr[j];
39                 arr[j] = temp;
40             }
41         }
42     }
43 }

两个算法的运行结果如上图。

 

posted @ 2013-09-09 19:50  骑着单车滑翔  阅读(575)  评论(0编辑  收藏  举报