关于抽样(取不重复的随机数集合)问题

来源:互联网 发布:风险矩阵图依据 编辑:程序博客网 时间:2024/06/05 10:54

概述

最初接触这个问题是在写纸牌游戏的时候,那时候还在看李刚写的《疯狂Java讲义》,里面有一个课后题就是完成网页版的纸牌游戏。洗牌发牌之前的第一步,也就是打乱纸牌的原有顺序。或许很多人都和我有一样的想法,第一个跳入脑海的是集合的方式,用一个空的集合来存储被随机取到的数字,在取后续数字的过程中不断的与集合中的数字进行比较,如果存在则重取,如果不存在那么加入集合,如此循环直至结束。
但是如果你和我的写法一样,那么你会惊奇的发现,这个东西和自己想的完全不一样,很久,很久,都没有完成洗牌的操作。(从而我对这种采用集合的方式有了一种武断的偏见,总有一种可能性的存在,使程序无限的运行下去。)很可能有人想到了交换两个随机索引位置的数字来实现洗牌,这样可以确保在O(n)的时间内完成洗牌。对于这种方法我没有绝对的意见,但是如果你看完了本文“随机解法的验证方法”部分之后,仍然觉得这种方法不错的话,我更不会有任何的意见。
这里先简单的进行下说明,(不怕读者不继续看继续的章节)如果使用的是Java语言大可以用Joshua开发的集合库中的shuffle方法,来完成洗牌的操作。

抽样问题的4种解法

本节的抽样问题不完全等同于概述中谈到的洗牌问题。本节问题可以描述成:从n个数中随机的选取m个不重复的数字。以下将要说到的四种方法均源自Jon Stanley的《编程珠玑》第一册。

集合方法1

这种方法一般都是首先映入程序员眼帘的,它和题目的描述思路正好相符,即从n中不断的取出一个随机数字放入到集合中,如果已存在则放弃重选,否则加入集合。代码如下:
initialize set S to emptysize = 0while size < m        t = bigrand() % n        if t is not in S               insert t to S               size++
对于上述代码中的bigrand()方法,在《编程珠玑》的12章课后第一题有说明:C库中的rand方法仅能返回16位大小的整数,如果要返回32位即2^16~2^32的数字值需要使用题中给出的bigrand()方法。
就像我在概述中说到的,第一眼看到这个算法的时候,我就立马在旁边写下了这样的文字“很不稳定,很可能会运行无法结束”。如果读者仔细看了习题的第三题,或许会和我一样对这个算法有了不同的认识。当m<n/2的时候,我们理论上可以在<=n的时间内完成抽样过程。

唐纳德方法S

这个方法绝对是神级的方法,并且唐纳德给出了证明。方法采用一个循环,逐个筛查给出的n个数字,却完全符合概率的随机性要求。方法如下:
select = mremaining = nfor i <- 0:n      if rand() % remaining < select                print i                select--;     remaining--;
方法很简单:在上述代码中的remaining代表还有多少数字可以被选择,select代表在大小为remaining的集合中选出多少个数字。[rand() % remaining]获得的是0~remaining之间的一个随机数,这个随机数<select的概率恰好是select/remaining,刚好符合概率论的选数规律即在remaining个数字中选出select个数字的概率为select/remaining。

唐纳德方法P(洗牌方法)

文中给出了洗牌的一种方法,即先打乱n个数字的顺序,然后从中选出前m个数字作为结果。
int i, j;int *x = new int[n]for (i=0; i< n; i++)       x[i] = i;for (i=0; i<m; i++)       j = randint(i, n-1);       int t = x[i];       x[i] = x[j];       x[j] = t;
这里需要说明的一点是:文中提到的时间复杂度O(n + mlogm),n要包括2,3两句代码初始化x[]数组的时间。

集合方法2(Floyd方法)

文中给这个方法的评价是“特别聪明的基于搜索的方法”,特别聪明是可以理解的,但是“基于搜索”这四个字让我有点糊涂。代码如下:
set S to emptyfor (i=n; i>n-m; i--)      int j = rand() % (i+1);      if j is not in S             insert j into S      else             insert i into S

这种方法确实很厉害,厉害就厉害在能够在O(m)的时间内完成抽样的任务。保证能够恰好取到m个数字的关键就在else处,至于所选数字的随机性好不好,作者没有验证过,有兴趣的时候一定要测试一把。关于测试随机解法的方法见以下一节。

随机解法的验证方法

像上边的其他章节一样,这一章节也不是我的原创,出处在[http://news.cnblogs.com/n/164312/]陈浩所写的如何测试洗牌程序一文。
测试的原理是:经过很多很多次的程序执行,记录每个数字出现的次数,统计出现次数是不是基本相等,即每个数字的“出镜率”是否均等。
陈浩给出的正确洗牌方法的代码:
void ShuffleArray_Fisher_Yates (char* arr, int len){    int i = len, j;    char temp;     if ( i == 0 ) return;    while ( --i ) {        j = rand () % (i+1);        temp = arr[i];        arr[i] = arr[j];        arr[j] = temp;    }}
我尝试用概率论的方式理解他的程序:第一轮选择时,每个数字被选中的概率为1/n,第二论选择时因为不知道第一轮哪个数字被选中,那么再次选择时每个数字的被选中的概率仍然是1/n,以此类推,所以他的方法有近乎完美的结果。这里只是我的理解,因为这个理解在我学概率论的时候就一直让我很迷惑,现在仍然是这样,对与不对还请方家不吝赐教。

补充:

在重看陈浩的文章是,看到了这个链接[http://weibo.com/1709648133/z64gWbUGp]读者不妨也看一下,能够有更新的理解。

原创粉丝点击