关于抽样(取不重复的随机数集合)问题
2013-05-04 20:49
218 查看
概述
最初接触这个问题是在写纸牌游戏的时候,那时候还在看李刚写的《疯狂Java讲义》,里面有一个课后题就是完成网页版的纸牌游戏。洗牌发牌之前的第一步,也就是打乱纸牌的原有顺序。或许很多人都和我有一样的想法,第一个跳入脑海的是集合的方式,用一个空的集合来存储被随机取到的数字,在取后续数字的过程中不断的与集合中的数字进行比较,如果存在则重取,如果不存在那么加入集合,如此循环直至结束。但是如果你和我的写法一样,那么你会惊奇的发现,这个东西和自己想的完全不一样,很久,很久,都没有完成洗牌的操作。(从而我对这种采用集合的方式有了一种武断的偏见,总有一种可能性的存在,使程序无限的运行下去。)很可能有人想到了交换两个随机索引位置的数字来实现洗牌,这样可以确保在O(n)的时间内完成洗牌。对于这种方法我没有绝对的意见,但是如果你看完了本文“随机解法的验证方法”部分之后,仍然觉得这种方法不错的话,我更不会有任何的意见。
这里先简单的进行下说明,(不怕读者不继续看继续的章节)如果使用的是Java语言大可以用Joshua开发的集合库中的shuffle方法,来完成洗牌的操作。
抽样问题的4种解法
本节的抽样问题不完全等同于概述中谈到的洗牌问题。本节问题可以描述成:从n个数中随机的选取m个不重复的数字。以下将要说到的四种方法均源自Jon Stanley的《编程珠玑》第一册。集合方法1
这种方法一般都是首先映入程序员眼帘的,它和题目的描述思路正好相符,即从n中不断的取出一个随机数字放入到集合中,如果已存在则放弃重选,否则加入集合。代码如下:initialize set S to empty size = 0 while 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 = m remaining = n for 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 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 empty for (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]读者不妨也看一下,能够有更新的理解。相关文章推荐
- 关于C++随机数生成中种子值设置的一点总结,解决随机数序列重复问题
- 关于C++随机数生成中种子值设置的一点总结,解决随机数序列重复问题
- 关于Hibernate中fatch=eager的bag集合(一个java List)使用Criteria查询出现重复记录的问题
- 关于随机数无重复填充数组问题
- 关于 取 多个随机数的不重复问题
- 关于ListView中继承BaseAdapter重写getview的显示重复数据或报转换错误的问题
- 关于HTTP客户端重复发送请求的问题
- 关于 最长的子字符串不重复字符 相关js算法问题
- 关于ListView的Adapter,解决ListView滚动后内容重复的问题
- 关于springMVC拦截器重复调用问题
- Hibernate中关于多表连接查询hql 和 sql 返回值集合中对象问题
- 关于生成不重复序号的问题
- 一个关于去除数组重复元素的问题(C语言实现)
- 关于 最短路条数 和 边不可重复最短路条数问题 /hdu3599(边不可重复最短路)
- 浅析Java中关于随机数Random的一些问题
- 问题:关于坛友的一个定时重复显示和隐藏div的实现
- 算法:关于生成抽样随机数的这些算法
- 关于生成不重复随机数组的问题?
- Hibernate中关于多表连接查询hql 和 sql 返回值集合中对象问题
- 关于delphi下clientsocket的重复连接问题