核心要点
构造均匀大空间:(rand7()-1)*7 + rand7() 得到 1..49 上的均匀整数。
取能被 10 整除的前缀:保留 1..40,对其余 41..49 拒绝并重抽。
映射:把 1..40 用 (num-1)%10 + 1 折叠到 1..10,等概率。
期望调用:每轮成功概率 40/49,期望约 49/40·2 ≈ 2.45 次 rand7。
标准回答
一、核心回答可以先说核心思路:先造一个更大的均匀空间,再只保留能被 10 平均分的部分。一、两次 rand7 造出 1 到 49。 令 num = (rand7() - 1) * 7 + rand7()。因为两次 rand7 独立均匀,这样可以得到 1..49 上的均匀整数。二、只接受 1 到 40。 二、关键机制
40 可以被 10 整除,所以如果 num 在 1..40,就接受;如果在 41..49,就丢掉重抽。这里的关键是:被接受的 1..40 仍然是均匀的。三、把 1 到 40 折叠成 1 到 10。 返回 (num - 1) % 10 + 1。因为 1..40 里每个结果 1..10 都正好出现 4 次,所以最终每个数概率都是1/10。三、面试补充可以补一句:不能直接对 49 取模,因为 49 不是 10 的倍数,会让某些结果多一次。拒绝 41..49 的目的,就是把偏差切掉。期望调用次数大概是 2 × 49/40,也就是 约 2.45 次 rand7 473。
import random
def rand7():
"""题目给定:等概率返回 1..7"""
return random.randint(1, 7)
def rand10():
"""等概率返回 1..10,拒绝采样保证均匀"""
while True:
num = (rand7() - 1) * 7 + rand7() # 均匀落在 1..49
if num <= 40: # 接受 1..40
return (num - 1) % 10 + 1 # 折叠到 1..10
# 41..49:拒绝,重新循环
if __name__ == '__main__':
from collections import Counter
cnt = Counter(rand10() for _ in range(1000000))
for k in range(1, 11):
print(k, round(cnt[k] / 1000000, 3)) # 应都 ≈ 0.1
常见误区
⚠️ 常见踩坑
误区一:直接对 49 取模。 49 不是 10 的倍数,直接取模会让部分数字概率更高。
误区二:把 41..49 也硬映射到 1..10。 这样等于重新引入偏差,拒绝采样就失去意义了。
追问
追问 1:能否复用被拒绝的 41..49 以减少 rand7 调用?
可以。41..49 本身也带着一些均匀随机性,可以继续和新的 rand7 组合,减少平均调用次数。不过面试里通常讲出“剩余熵可以复用”这个思路就够了。
追问 2:一般地,如何用 randM 实现 randN?
通用做法是:用多次 randM 拼出一个足够大的均匀空间,然后取里面最大的 N 的倍数,能整除就映射,不能整除的尾巴就拒绝重抽。
🔗 相似问题
同一考点的不同问法,换着练更稳
没找到想看的面试题?把你想看的告诉我们 →
延伸学习
按主题分类的相关资源,便于系统复习
