【算法面试高频题】rand7()问题

733 阅读5分钟

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播

1. 题目

已知有个rand7()的函数,返回1到7随机自然数,让利用这个rand7()构造rand10()随机1~10。

2. 解析

这道题主要是考察的对概率的理解。

要保证rand10()在整数1-10的均匀分布,可以构造一个1-10n 的均匀分布的随机整数区间(n为任何正整数)。假设x是这个1-10n区间上的一个随机整数,那么x%10+1就是均匀分布在1-10区间上的整数。由于(rand7()-1)*7+rand7()可以构造出均匀分布在1-49的随机数(原因见下面的说明),可以将41~49这样的随机数剔除掉,得到的数1-40仍然是均匀分布在1-40的,这是因为每个数都可以看成一个独立事件。

在进入题目之前,我想先给读者举个例子。设想一个简单的例子,假设已知rand2()可以均匀的生成[1,2]的随机数,现在想均匀的生成[1,4]的随机数,该如何做呢?

我想如果你也像我一样第一次接触这个问题,那么很可能会这么考虑——令两个rand2()相加,再做一些必要的边角处理。如下:

rand2() + rand2() = ? ==> [2,4]
   1    +   1     = 2
   1    +   2     = 3
   2    +   1     = 3
   2    +   2     = 4

// 为了把生成随机数的范围规约成[1,n],于是在上一步的结果后减1
(rand2()-1) + rand2() = ? ==> [1,3]
   0       +   1     = 1
   0       +   2     = 2
   1       +   1     = 2
   1       +   2     = 3

可以看到,使用这种方法处理的结果,最致命的点在于——其生成的结果不是等概率的。在这个简单的例子中,产生2的概率是50%,而产生1和3的概率则分别是25%。原因当然也很好理解,由于某些值会有多种组合,因此仅靠简单的相加处理会导致结果不是等概率的。

仔细观察上面的例子,我们尝试对 (rand2()-1) 这部分乘以 2,改动后如下:

(rand2()-1) × 2 + rand2() = ? ==> [1,3]
   0            +   1     = 1
   0            +   2     = 2
   2            +   1     = 3
   2            +   2     = 4

神奇的事情发生了,奇怪的知识增加了。通过这样的处理,得到的结果恰是[1,4]的范围,并且每个数都是等概率取到的。因此,使用这种方法,可以通过rand2()实现rand4()。那么尝试下这个例子:

(rand9()-1) × 7 + rand7() = result
     a               b

image.png 可以看到,这个例子可以等概率的生成[1,63]范围的随机数。那么其实我们可以得到如下规律:

已知 rand_N() 可以等概率的生成[1, N]范围的随机数
那么:
(rand_X() - 1) × Y + rand_Y() ==> 可以等概率的生成[1, X * Y]范围的随机数
即实现了 rand_XY()

现在我们将得到的规律运用到此题当中来。

首先rand7()-1得到一个离散整数集合{0,1,2,3,4,5,6},其中每个整数的出现概率都是1/7。那么(rand7()-1)7得到一个离散整数集合A={0,7,14,21,28,35,42},其中每个整数的出现概率也都是1/7。而rand7()得到的集合B={1,2,3,4,5,6,7}中每个整数出现的概率也是1/7。显然集合A和B中任何两个元素组合可以与1-49之间的一个整数一一对应,也就是说1-49之间的任何一个数,可以唯一确定A和B中两个元素的一种组合方式,反过来也成立。由于A和B中元素可以看成是独立事件,根据独立事件的概率公式P(AB)=P(A)P(B),得到每个组合的概率是1/71/7=1/49。因此(rand7()-1)*7+rand7()生成的整数均匀分布在1-49之间,每个数的概率都是1/49。

有人可能会疑惑,为什么不乘 6,乘 5呢?因为它不是等概率生成,只有乘7才能使得结果是等概率生成的。

所以,方法是:

1.rand7执行两次,出来的数为a1=rand7()1a2=rand7()1a_1=rand7()-1,a_2=rand7()-1.

2.如果a_17+a_2<40,b=(a_17+a_2)/4+1;如果a_1*7+a_2>=40,重复第一步。

int rand10()
{
    int x=0;
    do
    {
        x=(rand7()-1)*7+rand7();
    }while(x>40);
    return x%10+1;
}

进阶

但其实细心的人会发现,我们一滩会舍弃41-49的数,留下1-40的数来进行等概率的取。如果大于40的话,while循环仍然会继续。为了提高效率,对于大于40的随机数我们不舍弃,利用9个数进行操作。

1.(大于40的随机数−40−1)∗7+rand7()。这样我们可以得到 1-63之间的随机数,只要舍弃3个即可,那对于这3个舍弃的,还可以再来一轮:

2.(大于60的随机数 - 60 - 1) * 7 + rand7()(大于60的随机数−60−1)∗7+rand7()

这样我们可以得到1-21之间的随机数,只要舍弃1个即可

int rand10(){
    while (true){
        int num = (rand7() - 1) * 7 + rand7();
        // 如果在40以内,那就直接返回
        if(num <= 40) return 1 + num % 10;
        // 说明刚才生成的在41-49之间,利用随机数再操作一遍
        num = (num - 40 - 1) * 7 + rand7();
        if(num <= 60) return 1 + num % 10;
        // 说明刚才生成的在61-63之间,利用随机数再操作一遍
        num = (num - 60 - 1) * 7 + rand7();
        if(num <= 20) return 1 + num % 10;
    }    
}

归纳总结 randm()->randn()

已知random_m()随机数生成器的范围是[1, m] 求random_n()生成[1, n]范围的函数,m < n && n <= m*m 一般解法:

int random_n()
{
    int val = 0;
    int t;   //t为n的最大倍数,且满足t<m*m
    do
    {
        val = m * (random_m() - 1) + random_m();
    }while(val > t);
    return val;
}

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播