本文选取面试过程中有意思的概率问题来进行讨论和分享,具体如下:
给定一个函数rand7(),每次调用都能够等概率的生成1-7这7个数字,利用这个函数如何等概率地生成1~9呢,即实现函数rand9()。
分析
我们能够利用的有限条件是rand7()这个函数,它能够等概率地生成1-7,而我们需要等概率地生成1~9,这中间必然是有联系,且能够进行转换的。
换一个角度,如果我们有rand9(),那么如何实现rand7()呢?
实际上,如果我们有rand9()这个函数的话,每次生成的数据是1,2,3,4,5,6,7,8,9;对于数字8和9其实并不是我们需要的,我们直接丢弃就可以了,再次获取rand9()的值,直至小于等于7。
int rand7(){
int num = rand9();
while(num>7){
num = rand9();
}
return num;
}
但是这种丢弃元素重新获取的方式能够保证等概率地获取1~7这7个数字么?是公平的么?
我们可以实际求取一下这样获得数字1的概率,有以下情况:
-
第一次调用rand9(),获得了1,概率p为
-
第二次调用rand9(),数字大于7,再次调用rand9(),获得了1,概率p为
-
第三次调用rand9(),数字大于7,后续再调用了两次rand9(),获得了1,概率p为
-
第n次调用rand9(),获得1的概率为
由此可以得出用这种方式获取到元素1~7的概率
可知,这种方式是能够等概率地获取1~7每一个数字的。这种方式也称之为拒绝采样,满足一定条件就接受,不满足就拒绝。
基于这个认识,即
m,n为正整数,如果能够等概率地获取1-m这m个数字,那么同样可以等概率地获取1-n(n<m)这n个数字。
此时,我们可以回过头来看本文最初的问题,即给定一个rand7(),如何实现rand9()。这个问题与我们的具体问题相比较,等概率生成的范围较小,而要求的范围较大。如果我们能够基于rand7(),实现一个更大范围的等概率数(这个数的至少比9大),那么就可以通过拒绝采样的方式,筛选出小于9的数,实现rand9()。
联想
要求:基于rand7(),实现一个randk(),k>9
rand7()函数能够等概率生成1,2,3,4,5,6,7;
那么2*rand7()就能等概率生成2,4,6,8,10,12,14,但是中间的数1,3,5,7,9,11,13都没有生成;
7*rand7()就能等概率生成7,14,21,28,35,42,49,中间的差值都是7;
可以看到7*rand7()等概率生成的这些数,中间的差值都是7,因此可以利用rand7()生成1-7来填补中间这些数。
由此可以实现7*(rand7()-1)+rand7(),能够等概率返回 1~49这49个数,那么再筛选过程中,去掉其中大于9的数,肯定是能够实现rand9(),即:
int rand9(){
//实现1-49
int num = (rand7()-1)*7+rand7();
while(num>9){
num = (rand7()-1)*7+rand7();
}
return num;
}
这个方法虽然是对的,但是耗时会很多,因为很多大于9的数都被过滤了,需要多调用几次rand7(),时间复杂度高,获取1~9的数学期望会很高。可以基于此作一些改进:
int rand9(){
//实现1-49
int num = (rand7()-1)*7+rand7();
//对于大于45的数,拒绝本次采样,开始下一轮采样
while(num>45){
num = (rand7()-1)*7+rand7();
}
//num 1~45,num%9 等概率返回0-8
return num%9+1;
}
可以在实现等概率范围的1-49范围内,选择9的最大倍数45,将大于45的数过滤,开启下次采样,实现等概率返回1-45,此时再对9进行取模,能够等概率返回0-8这9个数。通过这种方式,每次最多丢弃的数字为46~49,一次获得1-45范围的数的概率为
实际上对于要过滤的46~49这4个数字,还可以做一次映射
可以将46-49映射到1-28,在对大于27(在28范围内9的最大倍数)舍去就可,最终舍去的就是28一个数字,能够进一步提高效率。
rand7()实现rand9()映射关系图
int rand9(){
//实现1-49
int num = (rand7()-1)*7+rand7();
while(true){
num = (rand7()-1)*7+rand7();
if(num <= 45){
return num%9+1;
}
num = (num-45-1)*7+rand7();
if(num<=27){
return num%9+1;
}
//num=28 舍去
}
}
刚刚我们是从rand7生成rand9(),因为7<9,所以需要将数据范围扩充到7*7=49。如果要生成rand300()呢?实际上就可以在生成rand49()的基础上再乘以7,得到rand343(),然后筛选出小于等于300的数即可。
从这里我们可以总结规律:
已知randM(),求取randN(),其中M<N。
我们可以不断地调用randM()并进行映射,直到生成一个范围大于N的数。
从上述例子来看,我们每次扩充范围的时候,都是要乘以M,因此我们可以从M进制及位运算的角度来看下这个问题。
-
将N转化为M进制数
-
然后从N的高位开始使用randM()进行随机生成一个M进制数,如果生成的数小于等于高位上的数字,则进行下一个高位数字的生成与选择,否则在此高位继续使用randM()直至小于等于高位上的数字。
rand7()实现rand300()的7进制原理图
最后
本文对等概率生成数字问题做了一个分析,其核心思想就是,将复杂问题转化为经典的已知基础问题,最主要的有两个方面:
-
如何扩大生成的数字范围
-
如何保证生成的各个数的概率相同,即保证公平
参考
《程序员代码面试指南》
《Leetcode》
更多信息欢迎关注微信公众号:惊鸿只为卿