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

概述

最初接触这个问题是在写纸牌游戏的时候,那时候还在看李刚写的《疯狂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[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 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]读者不妨也看一下,能够有更新的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值