代表性面试主题

算法面试:如何从未知长度的数据流中均匀抽样?

编程题中等
OfferCC 编辑团队发布 更新

题干

数据流长度未知且可能无法存入内存,请设计一个算法在单次扫描中均匀随机抽取 k 个不同元素。要求使用 O(k) 额外空间,说明 k=1 与一般 k 的算法,证明每个已见元素最终以 k/n 的概率留在样本中,并讨论随机数、空流和加权抽样。

题干与适用场景

输入是只能顺序读取一次的数据流,长度 n 事先未知,目标是等概率选出 k 个不同元素。不能先把全部元素放进数组,也不能等读完后再随机选下标。Reservoir Sampling 用固定大小为 k 的 reservoir 解决这个约束:第 i 个元素到达时,以 k/i 的概率让它进入样本,并随机替换当前样本中的一个位置。

这道题考察随机化流式算法。Vitter 的论文研究了未知总长度的单次抽样,大学课程资料给出了均匀性归纳证明。核心分类是 coding,考察概率不变量与空间约束,不因应用于日志或数据平台改成 data。

面试官考察点

  • 能否从“未知 n、单遍、固定内存”识别 reservoir pattern。
  • 能否先讲清 k=1 的 1/i 替换概率,再推广到 k。
  • 能否证明处理完第 i 个元素后,每个元素入样概率都是 k/i。
  • 能否避免重复抽样、错误的 k/n 预知假设和有偏随机数。
  • 能否说明时间 O(n)、空间 O(k) 与加权抽样的边界。

回答前需要澄清的问题

  • k 是否为正整数?若 k <= 0 或超过流长度,返回约定是什么?
  • 元素是否可重复?“不同元素”是不同记录还是按值去重,必须先定义。
  • 流是否可能为空、无限长或中途失败?输出与可恢复策略不同。
  • 需要最终样本还是实时查看当前样本?实时查看不能改变均匀性证明。
  • 随机数生成器是否提供 [0,1) 或无偏整数?不能直接假设取模无偏。
  • 抽样是否带权或需要分层?加权目标不再是等概率 reservoir。

30 秒回答框架

“先处理前 k 个元素填满 reservoir。第 i 个元素(从 1 开始)到达后生成均匀整数 j,范围 [0, i-1];若 j < k,就用新元素替换 reservoir[j],否则丢弃。这样处理完 i 个元素后,每个元素都以 k/i 的概率在样本中:新元素直接以 k/i 进入,旧元素以 k/(i-1) 留存并乘以 1 - 1/i。单遍时间 O(n),空间 O(k)。”

分步骤深入解答

第一步:从 k=1 开始。

第一个元素必选;第 i 个元素以 1/i 的概率替换当前候选。处理完 i 个元素后,每个元素的留存概率相同,都是 1/i。

第二步:推广到 k 个。

前 k 个元素先填入 reservoir。之后第 i 个元素以 k/i 的概率进入;一旦进入,在 k 个槽位中均匀选择一个替换。用整数随机数 j ∈ [0, i-1] 实现时,j < k 就替换槽位 j。

第三步:写出伪代码。

text
reservoir = first k items
for i = k+1 .. n:
  j = uniformInteger(0, i-1)
  if j < k:
    reservoir[j] = item i
return reservoir

若流对象不能预先填充,应在计数 seen <= k 时追加元素,之后使用相同分支。随机整数必须覆盖完整区间且无偏。

第四步:证明新元素的概率。

处理第 i 个元素时,它被选入的概率是 k/i。进入后不会再被替换的概率是之后每一步都不替换它:第 t 步替换某个特定槽位的概率是 1/t,因此留存概率为 ∏(1 - 1/t) = (i)/(n);最终概率 k/i × i/n = k/n。

第五步:证明旧元素的概率。

归纳假设第 i-1 步每个旧元素在 reservoir 的概率是 k/(i-1)。第 i 步它被保留的概率为 1 - (k/i × 1/k) = 1 - 1/i,所以最终为 k/(i-1) × (i-1)/i = k/i。新旧元素都满足同一不变量。

第六步:分析复杂度与随机实现。

每个元素只处理一次,时间 O(n);reservoir 存 k 个元素,额外空间 O(k)。当 i 超过安全整数范围时,使用支持无偏大整数范围的随机 API,避免浮点精度让概率失真。

第七步:处理输入边界。

空流返回空样本;k = 0 返回空样本或抛出约定异常;k > n 在未知 n 的流中只能读到结束后返回实际数量或报错。若元素按值去重,需要额外集合,空间可能不再是 O(k)。

第八步:说明加权与分布式扩展。

加权抽样需要按权重定义目标分布,不能继续使用等概率替换;可讨论 weighted reservoir 或 Efraimidis-Spirakis key。分布式场景要合并各分片样本并携带计数/权重,简单拼接各节点 reservoir 会产生偏差。

高质量示范回答

“我会维护容量为 k 的 reservoir。先填入前 k 个元素;从第 i = k+1 个开始,生成无偏整数 j ∈ [0, i-1]。若 j < k,就用当前元素替换第 j 个槽位,否则丢弃。对第 i 个新元素,它进入概率是 k/i;进入后每个槽位等可能,因此任意旧元素在这一步被替换的概率是 1/i,留存概率是 (k/(i-1)) × (1-1/i) = k/i。归纳可得处理完 n 个元素后,每个元素都有 k/n 的入样概率。算法单遍 O(n) 时间、O(k) 空间。我会验证空流、k=1、k=0、重复记录和多次模拟的频率,并明确加权或分布式扩展需要重新证明。”

常见错误

  • 先存完整流再抽样 → 违反未知长度和内存约束 → 边读边维护 reservoir。
  • 第 i 个元素总以 1/k 进入 → 概率随 i 变化而失真 → 使用 k/i。
  • 用 random() % i → 范围不整除时有偏 → 使用无偏整数采样。
  • 替换槽位不均匀 → 某些样本组合概率更高 → 在 k 个槽位中均匀选择。
  • 把 k/n 当作处理中的进入概率 → n 未知且每步概率不同 → 按当前计数 i 更新。
  • 忽略重复值定义 → “不同”可能指记录或值 → 先澄清去重语义。
  • 声称分片样本直接拼接仍均匀 → 各分片大小不同会造成偏差 → 携带计数并重新合并。
  • 加权场景继续用等概率算法 → 目标分布改变 → 使用加权 reservoir 并给出新证明。

追问及应对

追问一:为什么新元素的替换概率是 k/i?

在第 i 个元素到达时,算法从 i 个位置中均匀选一个,前 k 个位置代表 reservoir;命中这 k 个位置的概率就是 k/i。

追问二:k=1 时如何证明公平?

第一个元素概率为 1;第 i 个新元素以 1/i 替换。任一旧元素以 (1/(i-1)) × (1-1/i) = 1/i 留存,因此归纳成立。

追问三:如何做无偏随机整数?

使用语言提供的均匀整数 API,或采用拒绝采样丢弃超出最大可整除区间的随机值。不要假设简单取模总是无偏。

追问四:如果流长度小于 k 怎么办?

读到流结束时返回所有实际元素,或按接口约定报错。关键是不能凭空填充样本,也要在题目前说明行为。

追问五:如何抽取带权样本?

定义每个元素的权重目标,再使用 weighted reservoir 的随机 key 或指数/对数变换;均匀 reservoir 的 k/i 证明不能直接复用。

追问六:如何合并分布式 reservoir?

每个分片需携带已读数量及足够的随机优先级或权重,合并时按全局抽样规则重新选择。直接等量拼接或随机截断会偏向小分片。

追问七:如何验证均匀性?

对固定短流重复运行大量次,统计每个元素入样频率并与 k/n 比较;同时测试边界和随机种子可复现性。统计检验只能发现实现偏差,不能替代概率证明。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具