目录
- [1. 题目理解](#1. 题目理解)
- [2. 为什么不能直接用数学公式?](#2. 为什么不能直接用数学公式?)
- [3. 核心解法:拒绝采样 (Rejection Sampling)](#3. 核心解法:拒绝采样 (Rejection Sampling))
- [4. 进阶优化:减少 rand7() 调用次数](#4. 进阶优化:减少 rand7() 调用次数)
- [5. 总结与对比](#5. 总结与对比)
1. 题目理解
目标 :实现一个函数 rand10(),返回 1 到 10 之间的均匀随机整数。
限制 :只能使用给定的 rand7() 函数,它返回 1 到 7 之间的均匀随机整数。
核心要求 :均匀分布 。这意味着生成 1、2、...、10 的概率必须完全相等,都是 \(1/10\)。
什么是"均匀随机"?
如果 rand10() 是均匀的,那么运行很多次后,数字 1 出现的次数应该约等于数字 10 出现的次数。如果某个数字出现的概率偏高,那就不是均匀随机。
2. 为什么不能直接用数学公式?
以下方法的思路相对直接,初学者容易直接想到,但它们都是错误的:
-
错误思路 1:取模
pythonreturn rand7() % 10 + 1原因 :
rand7()只能生成 1-7。1%10是 1,7%10是 7,永远生成不了 8、9、10。 -
错误思路 2:相加
pythonreturn (rand7() + rand7()) % 10 + 1原因 :两个随机数相加,结果不是均匀分布的。
例如:和为 2 只有 (1,1) 一种情况;但和为 8 有 (1,7), (2,6)...(7,1) 七种情况。8 出现的概率远大于 2。这违反了均匀性。
3. 核心解法:拒绝采样 (Rejection Sampling)
要生成均匀随机数,最可靠的方法是构造一个更大的均匀空间,然后从中"截取"我们需要的部分。
思路推导
-
扩大样本空间 :
调用两次
rand7(),我们可以得到一个二维坐标 \((a, b)\),其中 \(a, b \in [1, 7]\)。这就像掷两个 7 面的骰子。总共有 \(7 \times 7 = 49\) 种可能的组合。
由于
rand7()是均匀的,这 49 种组合中,每一种出现的概率都是 \(1/49\)。 -
映射到一维 :
我们可以把这 49 种组合映射到数字 1 到 49。
公式:
idx = (a - 1) * 7 + ba - 1的范围是 0 到 6。(a - 1) * 7的范围是 0, 7, 14, ..., 42。- 加上
b(1 到 7) 后,idx的范围正好是 1 到 49。 - 且 1 到 49 中每个数字出现的概率相等。
-
拒绝采样 :
我们需要 1 到 10 的均匀分布。
49 不能被 10 整除。如果我们直接把 1-49 映射到 1-10,会导致某些数字概率偏高。
策略 :找到 49 以内最大的、能被 10 整除的数,即 40。- 如果生成的
idx在 1 到 40 之间:我们接受它,并将其映射到 1-10。 - 如果生成的
idx在 41 到 49 之间:我们拒绝 它(丢弃),重新生成两个新的rand7()。
- 如果生成的
-
映射结果 :
对于接受的
idx(1-40),使用公式(idx - 1) % 10 + 1即可得到 1-10。- 1 -> 1, 11 -> 1, 21 -> 1, 31 -> 1 (共 4 次)
- 10 -> 10, 20 -> 10, 30 -> 10, 40 -> 10 (共 4 次)
- 每个数字被选中的概率完全相等。
解法一:标准拒绝采样(推荐)
这是最清晰、最标准的面试解法。
python
# The rand7() API is already defined for you.
# def rand7():
# @return a random integer in the range 1 to 7
class Solution:
def rand10(self):
"""
:rtype: int
"""
while True:
# 1. 调用两次 rand7(),生成两个独立的随机数 a 和 b
a = rand7()
b = rand7()
# 2. 将二维坐标 (a, b) 映射为一维数字 idx,范围是 [1, 49]
# 解释:(a-1)*7 产生 0, 7, 14...42
# 加上 b (1-7) 后,产生 1 到 49 的均匀分布
idx = (a - 1) * 7 + b
# 3. 拒绝采样
# 我们只保留 1 到 40 的数字,因为 40 是 10 的倍数
# 41 到 49 的数字会被丢弃,重新循环生成
if idx <= 40:
# 4. 将 1-40 映射到 1-10
# (idx - 1) % 10 产生 0-9,再加 1 变为 1-10
return (idx - 1) % 10 + 1
复杂度分析
- 时间复杂度 :期望时间复杂度为 \(O(1)\)。
- 每次循环生成 1-49 的概率是 1。
- 被接受(idx <= 40)的概率是 \(40/49\)。
- 期望循环次数 = \(1 / (40/49) = 49/40 = 1.225\) 次。
- 每次循环调用 2 次
rand7(),所以期望调用rand7()次数 = \(2 \times 1.225 = 2.45\) 次。
- 空间复杂度 :\(O(1)\),只用了几个变量。
4. 进阶优化:减少 rand7() 调用次数
题目进阶部分问:能否尽量少调用 rand7()?
在标准解法中,当 idx 在 41-49 之间时(共 9 个数),我们直接丢弃了。这浪费了随机性。
这 9 个数本身构成了一个均匀的 rand9()。我们可以利用这部分的随机性,结合新的 rand7() 继续生成,而不是完全重来。
优化思路
- 第一轮 :生成 1-49。
- 若 1-40:返回结果。
- 若 41-49(9 个数):保留,视为
rand9()。
- 第二轮 :利用上一轮的 9 个数,再调用一次
rand7()。- 新空间大小:\(9 \times 7 = 63\)。
- 公式:
idx = (prev_rand9 - 1) * 7 + rand7(),范围 1-63。 - 若 1-60:返回结果(60 是 10 的倍数)。
- 若 61-63(3 个数):保留,视为
rand3()。
- 第三轮 :利用上一轮的 3 个数,再调用一次
rand7()。- 新空间大小:\(3 \times 7 = 21\)。
- 公式:
idx = (prev_rand3 - 1) * 7 + rand7(),范围 1-21。 - 若 1-20:返回结果。
- 若 21:彻底丢弃,重新开始第一轮。
这种优化可以将期望调用次数从 2.45 降低到约 2.21 次。
解法二:优化版代码
python
class Solution:
def rand10(self):
"""
:rtype: int
"""
while True:
# --- 第一轮:生成 1-49 ---
a = rand7()
b = rand7()
idx = (a - 1) * 7 + b # 范围 [1, 49]
if idx <= 40:
return (idx - 1) % 10 + 1
# --- 第二轮:利用 41-49 (共 9 个数) ---
# idx - 40 得到 1-9 的均匀随机数
a = idx - 40 # 范围 [1, 9]
b = rand7()
idx = (a - 1) * 7 + b # 范围 [1, 63] (9 * 7)
if idx <= 60:
return (idx - 1) % 10 + 1
# --- 第三轮:利用 61-63 (共 3 个数) ---
# idx - 60 得到 1-3 的均匀随机数
a = idx - 60 # 范围 [1, 3]
b = rand7()
idx = (a - 1) * 7 + b # 范围 [1, 21] (3 * 7)
if idx <= 20:
return (idx - 1) % 10 + 1
# 如果到了这里 (idx == 21),说明运气很差,所有随机性都用光了
# 只能回到 while 开头,完全重新开始
5. 总结与对比
| 特性 | 标准解法 (解法一) | 优化解法 (解法二) |
|---|---|---|
| 核心思想 | 拒绝采样 | 拒绝采样 + 随机性复用 |
| 代码可读性 | ⭐⭐⭐⭐⭐ (非常清晰) | ⭐⭐⭐ (逻辑稍复杂) |
| rand7() 期望调用次数 | 2.45 次 | ~2.21 次 |
| 面试建议 | 首选。逻辑清晰,不易出错。 | 进阶。如果面试官追问优化,再提出此思路。 |
建议:
- 理解公式 :
(a - 1) * 7 + b是将两个独立随机变量组合成一个更大范围均匀分布的标准技巧(进制转换思想)。 - 理解拒绝:为什么是 40 而不是 49?因为 49 无法被 10 整除。为了保证均匀,必须舍弃多余的部分。
- 死循环问题 :虽然代码里有
while True,但因为每次循环都有大于 0 的概率成功(40/49),所以理论上几乎不可能无限循环,期望次数是有限的。