核心要点

  • 场景:数据流总量 n 未知或无法一次载入内存,需等概率抽取 k 个样本。

  • 算法:前 k 个直接入池;第 i 个元素(i>k)以 k/i 的概率替换池中随机一个位置。

  • 正确性:任一元素最终留在池中的概率恒为 k/n,与位置无关。

  • 复杂度:单遍扫描 O(n) 时间、O(k) 空间。

标准回答

一、先把结论讲清楚

可以先说使用场景:数据流很长、长度 n 事先不知道,或者没法全部放进内存,但还想等概率抽样

一、k=1 时怎么做?

维护一个结果 res。遍历到第 i 个元素时,以 1/i 的概率把 res 替换成当前元素。这样第 1 个元素虽然一开始必选,但后面每一步都有可能被替换;

二、拆开机制和判断点

第 i 个元素虽然来得晚,但它入选概率就是 1/i。最后可以证明每个元素留下来的概率都是 1/n

二、k 个样本怎么做?

前 k 个元素先直接放进池子。之后遍历到第 i 个元素时,以 k/i 的概率决定是否让它入池;如果入池,就随机替换池子里的一个位置。

三、补上例子、边界和取舍

最终每个元素被选中的概率都是 k/n

三、复杂度是什么?

它只扫一遍数据,时间 O(n),池子大小固定是 k,所以空间 O(k)。需要强调:这题的价值不在代码长,而在“流式、未知长度、等概率”这三个关键词。

python
import random

def reservoir_sample_one(stream):
    """k=1: 从数据流中等概率抽取 1 个元素"""
    res = None
    for i, x in enumerate(stream, start=1):  # i 从 1 计数
        if random.randint(1, i) == 1:        # 以 1/i 概率替换
            res = x
    return res

def reservoir_sample_k(stream, k):
    """一般 k: 等概率抽取 k 个元素,每个被选概率 k/n"""
    pool = []
    for i, x in enumerate(stream, start=1):
        if i <= k:
            pool.append(x)                   # 前 k 个直接入池
        else:
            j = random.randint(1, i)         # j 在 [1, i]
            if j <= k:                        # 以 k/i 概率入池
                pool[j - 1] = x               # 替换池中随机位置
    return pool

if __name__ == '__main__':
    # 频率验证:每个元素被选概率应约为 k/n
    from collections import Counter
    n, k, trials = 10, 3, 200000
    cnt = Counter()
    for _ in range(trials):
        for v in reservoir_sample_k(range(n), k):
            cnt[v] += 1
    for v in range(n):
        print(v, round(cnt[v] / trials, 3))  # 应都 ≈ k/n = 0.3

常见误区

⚠️ 常见踩坑

误区一:用固定概率替换。 比如一直用 1/2,会让后来的元素更容易留下,破坏等概率。

误区二:只会背 k/i,但说不出场景。 面试官通常想听到“数据流、长度未知、内存有限”。

追问

追问 1为什么 k=1 时第 1 个元素和第 n 个元素被选概率相同?

第 1 个元素一开始必选,但它要一路不被替换,概率会乘成 1/n;第 n 个元素最后一步以 1/n 概率入选,而且没有后续替换。所以两者最终都是 1/n

追问 2如果要按权重加权采样怎么办?

可以用 A-Res 一类加权蓄水池方法:给每个元素按权重生成随机 key,然后保留 key 最大的 k 个。口头重点是:普通蓄水池是等概率,加权版本要让高权重元素更容易进入样本

🔗 相似问题

同一考点的不同问法,换着练更稳

延伸学习

按主题分类的相关资源,便于系统复习