非常感谢原版作者!
转自:http://www.cnblogs.com/wuyuegb2312/p/3141292.html#title4
本节主要受到《编程珠玑》第12章随机取样问题的启发,但不仅仅限于随机取样问题,进一步地,研究讨论了一些在笔试面试中常见的和随机函数以及概率相关的问题。
阅读本文所需的知识:
1.对C语言中或其他语言中等价的rand()、srand()有所了解。本文不讨论种子的设定和伪随机数的问题;
2.中学或以上水平的概率基本概念。
目录
问题1(《编程珠玑》习题12.1后半段):
给定一个rand(),可以产生从0到RAND_MAX的随机数,其中RAND_MAX很大(常见值:16位int能表示的最大整数32767),写出利用rand()生成[a,b]中任意整数的函数,其中a>=0, b<=RAND_MAX,且b-a<
分析:
这是在编程工作最常见的随机函数的应用,在这里做一个起点再合适不过。把随机数区间的起点从0变为a,同时把一共RAND_MAX+1个数的区间缩小至只含有b-a+1个数的区间,写为 a + rand()%(b-a+1),此时显然最大值是a+(b-a)= b。
进一步地,这个b-a<
附加思考:
如果b-a和RAND_MAX很接近会发生什么情况?读者不妨先做思考,问题2的分析会做出解答。
这个rand(),其实相当于《编程珠玑》提到的bigrand()。
问题2(笔试题变形):
给定一个随机数函数rand7(),它能以等概率生成1~7这7个整数。请根据rand7()写出类似的rand5()。
分析:
如果直接像问题1中一样,把1+rand7()%5作为rand5()会有什么情况发生?这时确实能产生1~5的随机数没错,可是各个数的概率相等吗?
对于随机数2,既有可能来自于1+1%5,也有可能来自于1+6%5,显然其概率是2/7,而不是1/5,不满足rand5()等概率产生各随机数这一隐含要求。不同于问题1,问题1中一个很大的区间收缩成较小区间时,各个元素映射后的新元素概率虽然概率可能不完全一样,但却是近似相同的。
为了满足等概率的要求,可以这么做:
int rand5() { int res; do { res = rand7(); } while(t>=6); return res; }
虽然保证了1~5的概率都变成了1/5,但是有一个无法避免的缺点是,每当产生了6或者7都要抛弃,相当于这一次运行是“空转”,浪费了时间。
如果对1/5这个概率不明白,可以有两种理解:每次产生6或7就被抛弃,剩下数的概率相等,必然为1/5;或者用更严密的推理:产生1~5的随机数,最终得到某一个的概率为:1/7+(1/7)*(2/7)+(1/7)*(2/7)2+...,无限项求和,结果是1/5。
问题3(笔试题原题)
给定一个随机数函数rand7(),它能以等概率生成1~7这7个整数。请根据rand7()写出类似的rand10()。
分析:
有了问题2的概率基础,把rand7()变成rand10()仅仅需要一点点思考了。
int rand10() { int t1,t2,res; do { t1 = rand7(); } while(t1>=6); //t1以等概率成为1/5 do { t2 = rand7(); } while(t2==7); //t2以1/2概率成为奇数或偶数 res = t1+5*(t2%2); //res是1~10中的任意一个数的概率都是1/10 //注意到%和*具有相同的优先级,这里去掉括号结果是错的 return res; }
问题2和问题3是对《编程珠玑》上使用范围不大的randint(i,j)生成其他范围随机数方法的解答。在掌握了问题2和问题3的解法后,你已经学会随机数区间的收缩和扩张,类似的问题迎刃而解。
问题4(《编程珠玑》习题12.1前半段)
C库函数rand()常返回15个随机位,写出bigrand(),能够返回30个随机位。
分析:
其实问题4和问题3有点像,但是不同之处在于,这次我们的视角是从位出发的,把rand()看做了将15个位每一个位以1/2概率设为0或1,从而生成0~RAND_MAX。从某种意义上来说,按这种理解方式来解这个问题更轻松一些,但是仅局限于2的幂减1这样的数值的区间,比如从0到11...11。在此基础之上把两部分的位拼接起来即可。
//《编程珠玑》的答案 //int bigrand() //{ return RAND_MAX *rand() +rand();} //最大值不是30个1,怀疑有错 //我的答案,把先生成的部分左移15位作为高15位 long bigrand() { return ((long)RAND_MAX+1)*rand() +rand();}
用随机事件表示随机事件?经过上面的区间收缩的思考,看上去并不难。把这个问题具体化为:使用rand()表示以M/N的概率发生的随机事件,M<=N,并可用作:if(事件A发生) ,其中P(A) = M/N,那么表示为:
if(rand()%N< M)
...
通过这种方式,我们就可以做出让程序“以M/N的概率执行某个命令”这样的设计了。
从概率角度出发
考虑整数0,1,2,...,n-1,可以用上节的方法以概率m/n选取0(推导方式略)。但是对于1,必须考虑之前0是否被选取而以(m-1)/(n-1)或m/(n-1)的概率选取1,后续就更加麻烦。好在迭代是计算机的长项,只需要把这个是否选择当前数的随机事件稍作修改即可,使之变成从r个其余的数中选择s个:
int gen(int m,int n) { int i, select = m,remaining = n; for(i=0;iif(rand() % remaining <select) { printf("%d\n",i); select--; } remaining--; } return 0; }
其概率证明可见于Knuth的The Art of Computer Programming第2卷。进一步地可以优化为:
int genknuth(int m,int n) { int i; for(i=0;iif(rand()%(n -i) < m) { printf("%d\n",i); m--; } return 0; }
《编程珠玑》提示,这种算法是“所有n的所有m元子集被选中的概率相等”,这个条件强于“所有元素被选中的概率相同”。下面是习题12.2中提到的“以等概率选择搜有元素,但是有些m元子集被选中的概率比其他子集大”的算法:直接选择1个数,则这个m元集合为它本身即后续的一共m个数,可能包括回绕。
对于这种方法,总要产生n次随机数。进一步可以写为for(i=0;i
int randselect(int m,int n) { int r; //assert(m<=n && m>=0); if(m>0) { r = rand()%n; if(r < m) { printf("%d\n",n-1); randselect(m-1,n-1); } else randselect(m,n-1); } return 0; }