随机算法讲义精读:从蓄水池抽样到Count-Min Sketch

发布时间:2026/10/14 9:02:26
随机算法讲义精读:从蓄水池抽样到Count-Min Sketch
简介《随机算法Sariel Har-Peled.pdf》是Sariel Har-Peled所著随机算法教材的PDF电子书面向已修过算法课程、能够进行证明的读者。书中开篇便从行政事务与先决条件讲起随后的“三个硬币”博弈直观展示随机策略如何对抗对手并与确定性算法作出对比相关章节还延伸到去随机化、对手论证与下界突破系统梳理随机化和平均情况分析的区别、样本空间与σ-代数、概率测度、期望和条件概率等基本概念适合课程学习或自学参考。资源为单个PDF文件压缩包大小8.78MB包含随机算法定义、优势、应用场景及基础概率理论尤其对游戏理论、优化问题和对抗性环境的算法设计有直接启发。单文件PDF便于直接阅读与检索整体内容围绕算法随机化理论展开可以帮助读者建立从概率基础到随机算法应用实践的完整认知。目前已有113人学习对算法研究者、竞赛选手及高年级本科生均有参考价值。1. 这份随机算法讲义为什么值得花一个月读完如果你做后端或数据工程迟早会遇到一类让人头疼的场景数据量大到没法精确计算但又必须给出一个答案。比如统计千万级 UV 里的去重数量、在几十亿条日志里找 TopK、判断一条新数据是不是见过。精确做要动用分布式任务、大内存、长耗时而业务只要一个“差不多对”的数。随机算法就是为这类场景而生的这份常被简称为 Har-Peled 讲义的《随机算法》PDF恰好把这类算法的原理和边界讲得最透彻。它不是市面上那本更偏数学证明的经典教材而是站在工程视角告诉你什么时候可以掷硬币、掷硬币之后误差有多大、怎么用十几个公式把误差压到可接受范围。适合有至少一年后端或数据处理经验、想把“概率”真正用进代码里的从业者。我最早接触这份讲义是在做实时风控特征统计的时候线上要维护一个超大的集合去重计数内存根本扛不住。当时第一反应是上 Redis后来读到这份讲义里的近似计数和蓄水池抽样才发现自己之前绕了远路。这篇笔记会把你从“听过随机算法”带到“能照着把蓄水池抽样、Count-Min Sketch、随机化矩阵验证写进自己的服务里”顺便把最容易翻车的几个坑提前踩平。2. 先搞懂讲义在讲什么四类随机算法与它们的工程坐标2.1 Las Vegas 与 Monte Carlo两类算法的本质差别随机算法内部有一条清晰的分界线Las Vegas 算法永远给出正确结果但运行时间是随机的Monte Carlo 算法运行时间可控但结果有概率出错。讲义在前几章就会反复围绕这条分界线展开因为它决定了你在工程里敢不敢用。随机化快速排序是 Las Vegas 的典型无论输入怎么排列结果都正确只是最坏情况下可能退化到 O(n²)随机化是为了把“遇到坏输入”的概率摊平。而布隆过滤器是 Monte Carlo 的典型它告诉你“肯定不在”是准确的但“可能存在”会有假阳性概率这个概率由位数组大小和哈希函数数量决定。工程选型时我一般会先问这个场景允许结果错吗允许的话是允许假阳性还是假阴性这个问题的答案直接导向不同的算法家族。2.2 讲义的主线脉络从尾不等式到随机数据结构这份讲义的结构大体上是一条从数学地基往上盖楼的路。开头复习概率论基础但不纠缠于测度论只把随机变量、期望、方差讲够用。紧接着是重头戏尾不等式——Markov、Chebyshev、Chernoff 三件套它们回答的是同一个问题随机变量的值偏离期望的概率有多大。这个问题的答案就是后面所有近似算法的误差依据。再往后进入算法本体蓄水池抽样与流式采样、近似计数、哈希与指纹技术、随机化矩阵乘法验证再到跳表这类随机数据结构。讲义里给每个算法都配了完整的概率分析但在工程落地时我更愿意把那些证明过程当成“参数计算器”——当 Chernoff 界告诉你“用这个宽度和深度误差超过 5% 的概率小于 1%”你真正需要的只是这两个数字而不是中间的推导步骤。2.3 和另一本经典教材的区别这份讲义的“口味”偏应用市面上还有另一本很有名的随机算法教材写法偏理论习题难度也更高。这份讲义相比之下更口语化时常带着作者的个人风格喜欢用几何直觉来解释抽象概念这跟作者在计算几何领域的研究背景有关。如果你只是想在工程里用上随机算法我建议从这份讲义入手配合少数几个关键公式去理解而不是一上来就啃那本厚重的经典。怎么高效读这份 PDF第一遍只看目录和每章末尾的习题圈出与流式处理、哈希、抽样相关的章节跳过纯数学推导第二遍再回头补尾不等式的证明因为你只有在写 Count-Min Sketch 时才会真正理解 Chernoff 界为什么要求样本独立。3. 把讲义里的三个算法搬进工程可直接复制的代码与参数3.1 蓄水池抽样流式场景下的等概率采样蓄水池抽样解决的问题是数据以流的形式到达总量未知需要从中均匀随机抽取 k 个样本。朴素做法是把所有数据存下来再随机抽但流式场景下做不到。讲义里的解法是维护一个大小为 k 的“蓄水池”前 k 个元素直接放进池子之后每个元素以 k/i 的概率决定是否替换池中元素。import random def reservoir_sample(stream, k): 从 stream 中均匀随机抽取 k 个样本。 stream 可以是迭代器/生成器不要求预先知道总量。 reservoir [] for i, item in enumerate(stream): if i k: reservoir.append(item) else: # 第 i 个元素从0开始计数i k被选中的概率是 k/(i1) j random.randint(0, i) if j k: reservoir[j] item return reservoir逻辑说明每次迭代只做一次随机数生成和一次数组替换时间复杂度 O(n)空间复杂度 O(k)。关键在于random.randint(0, i)这个闭区间在随着数据流增长而变大使得越晚到达的元素被选入的概率越小但最终整体上每个元素被选中的概率都是 k/NN 为流总长度。用这个算法做线上日志采样时可以直接接在 Kafka 消费端不用攒批每来一条就处理一条。参数说明k 的取值取决于下游任务需要的样本量。比如用采样后的数据做 AB 实验k 至少要满足最小样本量要求做模型训练k 则要考虑类别平衡。这个算法的局限在于每个元素只能被“最终是否保留”一次判断如果你需要支持“撤销”或“加权采样”部分元素权重更高就要换成加权蓄水池抽样讲义里有对应变体。另外要注意random.randint使用的是 Mersenne Twister伪随机序列对于绝大多数生产场景足够但如果你要对抗恶意构造的数据流例如有人故意让某个元素恰好落在替换点就需要改用密码学安全的随机源。3.2 Count-Min Sketch用固定内存近似统计频次Count-Min Sketch 解决的问题是在内存受限的前提下统计每个元素的出现次数允许误差但不允许“漏报”。核心结构是一个 d 行 w 列的二维计数数组每个元素经过 d 个不同的哈希函数映射到每行的一个位置计数加一。查询时取 d 个位置中的最小值作为该元素出现次数的估计。import mmh3 class CountMinSketch: def __init__(self, epsilon, delta): 根据误差上界 epsilon 和失败概率 delta 自动计算宽高。 标准结论: w ceil(e / epsilon), d ceil(ln(1 / delta)) import math self.w math.ceil(math.e / epsilon) self.d math.ceil(math.log(1.0 / delta)) self.table [[0] * self.w for _ in range(self.d)] def _hashes(self, key): 将 key 映射成 d 个桶下标用不同种子的 MurmurHash 保证独立性。 return [mmh3.hash(key, i) % self.w for i in range(self.d)] def add(self, key): for idx in self._hashes(key): self.table[i][idx] 1 def estimate(self, key): 返回 key 出现次数的估计值真实值 估计值。 return min(self.table[i][idx] for i, idx in enumerate(self._hashes(key)))逻辑说明估计值永远不会小于真实值因为每个桶的计数是所有哈希到该桶的元素之和但可能高估因为多个元素可能落到同一桶。Chernoff 界保证估计值与真实值之差超过 epsilon * N 的概率不超过 delta其中 N 是总计数。这正是“讲义里的尾不等式直接变成工程参数”的过程——你不需要自己推公式只需要把允许的误差率和可接受的风险率喂给构造方法。参数说明epsilon 越小w 越大内存占用线性增长delta 越小d 越大每次查询要算更多次哈希。我一般按业务容忍度来设做流量 TopK 监控时 epsilon 取 0.001、delta 取 0.01大约需要 3.6MB 内存对十万级 QPS 的服务完全无感。这里要特别提醒Python 的hash()函数不能用于这个场景因为进程重启后哈希种子会变化而且字符串哈希的分布质量不够稳定很可能导致某一行桶内碰撞严重估值偏高。用mmh3这类带种子参数的哈希才能构造出“近似独立”的多行哈希。3.3 随机化矩阵乘法验证把 O(n³) 变成 O(n²·k)第三个算法来自讲义的指纹技术章节解决的问题很实用你写了一个矩阵乘法函数怎么快速验证结果对不对朴素做法是把乘积算出来逐一比对但如果你本来就是在验证自己写的矩阵乘法这个办法没有意义。Freivalds 算法用一个随机向量把验证成本降到 O(n²)import random def verify_matmul(A, B, C, trials10): 验证 A * B C 是否成立。 返回 True 表示很可能正确False 表示一定不正确。 n len(A) for _ in range(trials): r [random.randint(0, 1) for _ in range(n)] # 先算 C·r再算 A·(B·r)利用结合律避免完整矩阵乘法 cr [sum(C[i][j] * r[j] for j in range(n)) for i in range(n)] br [sum(B[i][j] * r[j] for j in range(n)) for i in range(n)] abr [sum(A[i][j] * br[j] for j in range(n)) for i in range(n)] if cr ! abr: return False return True逻辑说明如果 A·B C那么对任意向量 rA·(B·r) C·r 恒成立。如果 A·B ≠ C那么随机选取的 0/1 向量 r 至少有 1/2 的概率能让两边不等。所以单次验证有 50% 的失败率把错的判成对的但重复 10 次后失败率降到 1/1024这在工程上已经足够可信。关键优化在计算顺序先算 B·rO(n²)再算 A·(B·r)O(n²)全程不需要做 n³ 的全量矩阵乘法。参数说明trials 每增加一次验证时间线性增加但失败率指数下降。我通常取 10~20取决于矩阵规模和业务对风险的容忍度。这个算法在 Spark 或分布式计算里特别有用如果你想在不下发上百 GB 数据的情况下确认两个集群算出来的结果是否一致先让每台机器对分片做随机投影再把投影结果汇总比对成本远低于全量比较。讲义里给出的原始版本还支持在整数环和有限域上运算如果矩阵是整数且你担心浮点误差可以改成对一个大素数取模后再验证。提示这三个算法的实现都以 Python 3.8 为准不依赖除 mmh3 外的第三方库。生产使用时建议把随机数生成器替换成secrets模块或独立随机源避免在 fork 多进程时所有子进程拿到相同随机序列。4. 读这份讲义和落地时的常见误区与排查经验4.1 把 Las Vegas 当成 Monte Carlo 用线上出现延迟毛刺现象用随机化快速排序重写了某个核心链路的排序逻辑理论上平均 O(n log n)但上线后偶尔出现单次请求耗时飙升甚至触发超时告警。原因随机化快速排序保证的是“期望时间”低不保证“每一次都快”。当随机种子恰好选出一个很差的 pivot 序列时单次运行会退化到 O(n²)。Las Vegas 算法的特点是结果正确但时间随机如果你把它的时间当作可控指标来承诺 SLO就会踩坑。解决先用蓄水池抽样或者直接洗牌打乱输入再跑快速排序把这层随机化放在排序之前而不是只依赖排序内部的 pivot 随机化更稳妥的是设置排序超时超时后降级到堆排序保证最坏情况 O(n log n)。讲义里对两者的界定非常清晰落地前先问自己一句我接受“偶发变慢”还是“偶发出错”两个答案分别对应 Las Vegas 和 Monte Carlo别混着用。4.2 为了“可复现”固定随机种子结果所有 worker 抽到相同样本现象训练数据采样脚本在单机上跑没问题部署到多机分布式任务后发现所有机器的采样结果完全一致模型训练几乎没有随机性精度反而下降。原因有人在初始化代码里写了random.seed(42)本意是让单测可复现但这个代码被放进了 worker 的初始化路径。每个 worker 进程启动后都从同一种子开始生成完全相同的随机序列蓄水池抽样变成了“等间隔采样”相当于随机性失效。解决把“可复现”和“随机性”两个目标拆开。单测里固定种子没问题但生产路径的随机源必须来自系统熵池os.urandom或secrets或者至少每个 worker 用主机名、进程 ID、时间戳组合出不同种子。如果确实需要整个分布式任务可复现优先保存“采样结果 ID 列表”而不是固定种子——这样下次重放时直接加载 ID 列表既保留随机性又能复现。4.3 哈希函数选型不当Count-Min Sketch 估值系统性偏高现象用 Count-Min Sketch 统计接口调用频次和精确计数对比后发现估值整体偏高 20% 以上且不是个别 key 的问题而是所有 key 都偏。原因用了 Python 内置的hash()对字符串做散列。它默认启用哈希随机化进程间不固定且对于短字符串的分布质量并不保证。当 sketch 宽度不大时多个热门 key 反复映射到相近桶位碰撞概率远超理论假设导致每行的计数都被污染取最小值后仍然高估。解决切换到 MurmurHash3 或 xxHash用不同种子派生 d 个哈希函数验证时要检查哈希结果的分布是否均匀——简单方法是取一万个真实 key把每个 key 的桶下标画出来看有没有明显聚集。我一般在调参前先做一步预检用一个小的精确计数器例如字典跑一小段真实流量和 sketch 的估值对比误差超过 2 倍就说明哈希分布可能有问题。4.4 Chernoff 界的独立性假设不满足误差远超理论界现象按照讲义给出的公式设置了 epsilon 和 delta跑仿真时误差却明显超出理论范围反复检查代码也没发现逻辑错误。原因Chernoff 界要求样本之间相互独立但真实数据往往自带相关性。比如统计“用户点击次数”时同一个用户的多次点击不是独立事件又比如按时间窗口采样的日志在流量高峰段落样密度自然偏高。抽样破坏了独立性尾不等式的结论自然失效。解决在做抽样之前先按业务身份维度用户 ID、设备 ID、会话 ID做分层或分组保证同一实体的多条记录不会被拆成多个独立样本或者改用带重复性的分组抽样方法。讲义里的理论结论都是在“理想独立”条件下的上界真实环境里用 1/10 的经验值做预算是比较稳妥的做法。4.5 把近似计数结果用于计费月底对不上账现象用 Count-Min Sketch 统计每个用户的调用次数月底出账单时客户发现数字偏高投诉。原因Count-Min Sketch 是单侧误差结构——估计值只高不低这是它的设计特性。在计费这种“分毫必争”的场景近似算法天然不适用不能用“误差很小”来搪塞哪怕 0.1% 的虚增也是无法接受的。解决近似计数只用于监控、限流、热榜这类允许误差的辅助场景主路径的计费用精确计数器或者用 Exact 计数配合定期快照落到数据库。如果内存实在有限可以做两级结构近似层先过滤掉绝大多数低频 key高频 key 再单独用精确计数维护这样能兼顾内存和准确性。5. 把这份讲义从“读过”变成“能用”三张速查卡第一张卡是“误差边界速查”Markov 不等式、Chebyshev 不等式、Chernoff 界这三个公式只需要记住它们的结论形态——Chernoff 界给出的误差上界是指数级的Chebyshev 是平方级的。遇到任何近似算法先问一句这个算法的误差是用哪个界证的如果答案是需要独立同分布你就要考虑数据相关性直接套用会吃暗亏。第二张卡是“场景-算法对照”流式采样用蓄水池频次统计用 Count-Min Sketch去重计数用 HyperLogLog讲义里的概率计数是其前身存在性判断用布隆过滤器验证一个黑盒矩阵乘法的结果用 Freivalds 算法。把这个对照表贴在自己的笔记工具里遇到“内存不够、数据太大、算不准”这几个关键词时先翻表多数情况能省掉半天方案调研时间。第三张卡是“参数实测 checklist”新写一个概率数据结构不要只调库必须做三件事——用真实流量跑小样本对照确认误差方向符合理论CM Sketch 只高不低布隆过滤器只有假阳性用不同随机种子跑多次确认结果方差可控用两倍预期流量压测确认哈希计算本身不成为性能瓶颈。我之前在线上把蓄水池抽样的随机数生成器换成random.random() k/n的比较式写法误把比较式当成等概率导致最后样本偏向流的前半段这就是不动手验证理论的教训。希望帮到你动手跑一段真实数据比读完十个章节都管用。本文还有配套的精品资源点击获取