池中锦鲤的自我修养,聊聊蓄水池算法

面试如泡池,蓄水似人生

起初你满怀期待跳进大厂池子,以为自己是天选之子,结果发现池子里早挤满了和你一样的“锦鲤候选人”。HR的渔网一撒,捞谁全看概率——这不就是蓄水池算法的精髓吗?

  1. 初入池(i≤k):前k个幸运儿直接进池,像极了校招提前批的“VIP通道”。
  2. 概率博弈(i>k):第k + 1个候选人开始,以k / i的概率被捞,同时池子里随机一位“前辈”出局。
    • “面完三面等消息?恭喜,你进入了k / N的量子纠缠态。”
  3. 公平玄学:无论你是第100还是第1000号候选人,最终被捞的概率都是k / N——池子越深,缘分越随机

所以,下次面试官问你“如何公平抽奖”,请优雅地回答:

“简单,像泡池子一样,先到的不一定稳,后来的未必凉,一切交给蓄水池的数学之美。”

毕竟,泡池子的终极奥义是:
“池中躺平,等一个伯乐;算法加持,信命不认输。”


机器持续吐出编号递增的球(1,2,3…),你有一个容量为k的袋子(k=10),怎么保证机器吐出第N个球时,每个球进入袋子的概率都是k/N?

直接上蓄水池采样规则:

阶段处理逻辑
前k个球直接放入袋子(100%保留)
第k+1个球起对第i个球:
- 以k/i概率决定是否保留
- 若保留,随机替换袋中一个球

每个球进入袋子的概率都是k / N的证明:

情况1:前k个球(1 ≤ i ≤ k)的保留概率推导

  1. 初始状态:前k个球直接入袋,保留概率为 1
  2. 处理第k+1个球时
    • k+1个球入袋的概率为 k/(k+1)
    • 若入袋,随机替换袋中某球的概率为 1/k,即第i号球被淘汰的概率为 (k/(k+1)) × (1/k) = 1/(k+1)
    • 因此,第i号球的保留概率为 1 - 1/(k+1) = k/(k+1)
  3. 处理第k+2个球时
    • k+2个球入袋的概率为 k/(k+2)
    • 淘汰第i号球的概率为 (k/(k+2)) × (1/k) = 1/(k+2)
    • 保留概率为 1 - 1/(k+2) = (k+1)/(k+2)
  4. 递推至第N个球
    • 每一步的保留概率依次为 k/(k+1), (k+1)/(k+2), …, (N-1)/N
    • 最终保留概率为各步概率的乘积:
      k k + 1 × k + 1 k + 2 × ⋯ × N − 1 N = k N \frac{k}{k+1} \times \frac{k+1}{k+2} \times \cdots \times \frac{N-1}{N} = \frac{k}{N} k+1k×k+2k+1××NN1=Nk

情况2:后N-k个球(k < i ≤ N)的保留概率推导

  1. 第i号球入袋时
    • 入袋概率为 k/i
  2. 处理第i+1个球时
    • i+1个球入袋的概率为 k/(i+1)
    • 淘汰第i号球的概率为 (k/(i+1)) × (1/k) = 1/(i+1)
    • 保留概率为 1 - 1/(i+1) = i/(i+1)
  3. 处理第i+2个球时
    • 保留概率为 (i+1)/(i+2)
  4. 递推至第N个球
    • 每一步的保留概率依次为 i/(i+1), (i+1)/(i+2), …, (N-1)/N
    • 最终保留概率为各步概率的乘积:
      k i × i i + 1 × i + 1 i + 2 × ⋯ × N − 1 N = k N \frac{k}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \times \cdots \times \frac{N-1}{N} = \frac{k}{N} ik×i+1i×i+2i+1××NN1=Nk

结论
无论球的编号i在哪个区间(1 ≤ i ≤ N),其最终留在蓄水池中的概率均为 k/N,即实现了等概率采样。


C++代码实现(改写左神Java版本)

#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>using namespace std;class Pool {
private:int size;vector<int> bag;public:Pool(int s) : size(s), bag(s) {}// 判断是否选择第i号球bool pick(int i) {return rand() % i < size;}// 随机选择袋子中的一个位置int where() {return rand() % size;}// 处理新进入的球void enter(int i) {if (i <= size) {bag[i - 1] = i;} else {if (pick(i)) {bag[where()] = i;}}}// 获取袋子中的球vector<int> getBag() {return bag;}
};int main() {srand(time(NULL)); // 初始化随机数种子cout << "测试开始" << endl;int n = 41;        // 一共吐出多少球int m = 10;        // 袋子大小多少int testTimes = 10000; // 进行多少次实验vector<int> cnt(n + 1, 0);for (int k = 0; k < testTimes; k++) {Pool pool(m);for (int i = 1; i <= n; i++) {pool.enter(i);}vector<int> bag = pool.getBag();for (int num : bag) {cnt[num]++;}}cout << "机器吐出到" << n << "号球, " << "袋子大小为" << m << endl;cout << "每个球被选中的概率应该接近" << (double)m / n << endl;cout << "一共测试" << testTimes << "次" << endl;for (int i = 1; i <= n; i++) {cout << i << "被选中次数 : " << cnt[i] << ", 被选中概率 : " << (double)cnt[i] / testTimes << endl;}cout << "测试结束" << endl;return 0;
}    
测试开始
机器吐出到41号球, 袋子大小为10
每个球被选中的概率应该接近0.243902
一共测试10000次
1被选中次数 : 2484, 被选中概率 : 0.2484
2被选中次数 : 2414, 被选中概率 : 0.2414
3被选中次数 : 2445, 被选中概率 : 0.2445
4被选中次数 : 2439, 被选中概率 : 0.2439
5被选中次数 : 2456, 被选中概率 : 0.2456
6被选中次数 : 2434, 被选中概率 : 0.2434
7被选中次数 : 2452, 被选中概率 : 0.2452
8被选中次数 : 2405, 被选中概率 : 0.2405
9被选中次数 : 2385, 被选中概率 : 0.2385
10被选中次数 : 2387, 被选中概率 : 0.2387
11被选中次数 : 2413, 被选中概率 : 0.2413
12被选中次数 : 2463, 被选中概率 : 0.2463
13被选中次数 : 2425, 被选中概率 : 0.2425
14被选中次数 : 2405, 被选中概率 : 0.2405
15被选中次数 : 2463, 被选中概率 : 0.2463
16被选中次数 : 2434, 被选中概率 : 0.2434
17被选中次数 : 2406, 被选中概率 : 0.2406
18被选中次数 : 2456, 被选中概率 : 0.2456
19被选中次数 : 2400, 被选中概率 : 0.24
20被选中次数 : 2467, 被选中概率 : 0.2467
21被选中次数 : 2403, 被选中概率 : 0.2403
22被选中次数 : 2415, 被选中概率 : 0.2415
23被选中次数 : 2432, 被选中概率 : 0.2432
24被选中次数 : 2438, 被选中概率 : 0.2438
25被选中次数 : 2464, 被选中概率 : 0.2464
26被选中次数 : 2514, 被选中概率 : 0.2514
27被选中次数 : 2416, 被选中概率 : 0.2416
28被选中次数 : 2546, 被选中概率 : 0.2546
29被选中次数 : 2440, 被选中概率 : 0.244
30被选中次数 : 2350, 被选中概率 : 0.235
31被选中次数 : 2398, 被选中概率 : 0.2398
32被选中次数 : 2483, 被选中概率 : 0.2483
33被选中次数 : 2405, 被选中概率 : 0.2405
34被选中次数 : 2472, 被选中概率 : 0.2472
35被选中次数 : 2449, 被选中概率 : 0.2449
36被选中次数 : 2431, 被选中概率 : 0.2431
37被选中次数 : 2494, 被选中概率 : 0.2494
38被选中次数 : 2515, 被选中概率 : 0.2515
39被选中次数 : 2507, 被选中概率 : 0.2507
40被选中次数 : 2384, 被选中概率 : 0.2384
41被选中次数 : 2411, 被选中概率 : 0.2411
测试结束

设计抽奖系统,在参与某厂招聘会的学生中,抽取100人送offer

等概率抽取100人,且学生不能重复报名(去重)。维护一个大小为100的蓄水池,只需要判断学生是否首次报名,并确定是第几个参与者即可。第i个首次报名的学生,以100 / i概率决定是否入池,若入选则随机替换池中一人。

✅ 单服务器轻量级维护
✅ 规避多节点数据汇总
✅ 实时性高,无最终计算延迟


蓄水池采样核心

在数据流持续输入且总量未知的情况下,用固定容量的容器维护一个等概率样本集。

典型场景及变种

  1. 判断条件
    • 数据流式输入且总量未知;
    • 需要等概率采样固定数量样本。
  2. 变种处理
    • 动态容量:将固定k改为动态k(i),概率调整为k(i)/i
    • 分布式:先本地采样再合并二次采样,利用分治保证概率正确性。
  • 日志采样:分布式系统中按1/1000比例抽样百万级日志,保留统计特征;
  • 推荐系统冷启动:维护固定大小的历史交互样本,保证新物品等概率曝光;
  • 实时弹幕过滤:从千万级弹幕中实时抽取固定数量展示;

为什么 MapReduce中要用蓄水池采样?

MapReduce 是一种处理海量数据的编程模型,核心思想是 “分而治之”,就像一群人分工合作完成大任务。

  • MapReduce 处理的往往是 TB 级甚至 PB 级数据(比如日志、用户行为数据),直接全量采样或分析不现实:
    • 全量存储消耗大量内存和磁盘;
    • 直接处理所有数据会拖慢计算速度。
    • 蓄水池采样能用固定大小的样本(比如 1000 条数据)代表整体特征,大幅减少计算量。
  • 如果采样不均匀(比如总选前 10% 的数据),样本就会失真,无法反映整体特征。
    • 蓄水池采样能确保每个数据被选中的概率相等。
    • 即使数据是动态流入的(不知道总量 N),也能保证等概率,这对 MapReduce 处理实时或未知总量的数据很关键。
  • MapReduce 的分布式架构适配蓄水池采样
    • Map 阶段:每个节点独立对本地数据做蓄水池采样(比如每个节点存 m 条样本),避免传输全量数据,减少网络开销;
    • Reduce 阶段:合并所有节点的样本(总共有 M = 节点数 × m 条),再用蓄水池采样取 m 条作为最终样本,保证全局等概率。

如何等概率采样1个元素,但数据量极大且只能遍历一次?

蓄水池采样的k=1特例,对第i个元素以1/i概率替换当前选中元素。

若数据以链表形式存储,如何高效实现蓄水池采样?

遍历链表时,对第i个节点(i从1开始)用k/i概率决定是否加入蓄水池,维护大小为k的数组,替换时随机选位置,时间复杂度O(n),空间O(m)

带权重的蓄水池采样如何实现?

选中概率改为权重之和的比例,元素i权重为w_i,第i步选中概率为w_i/(w₁+w₂+…+w_i),替换时按权重随机选择对象。

注:算法公平,但offer未必,建议多投几个池子。

参考

[1] 程序员代码面试指南
[2] 算法讲解107【扩展】大厂面试中经常漫聊的有趣算法问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/diannao/85524.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux应用开发之网络套接字编程

套接字&#xff08;Socket&#xff09;是计算机网络数据通信的基本概念和编程接口&#xff0c;允许不同主机上的进程&#xff08;运行中的程序&#xff09;通过网络进行数据交换。它为应用层软件提供了发送和接收数据的能力&#xff0c;使得开发者可以在不用深入了解底层网络细…

小白的进阶之路系列之六----人工智能从初步到精通pytorch数据集与数据加载器

本文将介绍以下内容: 数据集与数据加载器 数据迁移 如何建立神经网络 数据集与数据加载器 处理数据样本的代码可能会变得混乱且难以维护;理想情况下,我们希望我们的数据集代码与模型训练代码解耦,以获得更好的可读性和模块化。PyTorch提供了两个数据原语:torch.utils…

深入理解设计模式之中介者模式

深入理解设计模式之&#xff1a;中介者模式&#xff08;Mediator Pattern&#xff09; 一、什么是中介者模式&#xff1f; 中介者模式&#xff08;Mediator Pattern&#xff09;是一种行为型设计模式。它通过引入一个中介对象&#xff0c;来封装一组对象之间的交互&#xff0…

基于通义千问的儿童陪伴学习和成长的智能应用架构。

1.整体架构概览 我们的儿童聊天助手将采用典型的语音交互系统架构,结合大模型能力和外部知识库: 2. 技术方案分解 2.1. 前端应用/设备 选择: 移动App(iOS/Android)、Web应用,或者集成到智能音箱/平板等硬件设备中。技术栈: 移动App: React Native / Flutter (跨平台…

Python Day40

Task&#xff1a; 1.彩色和灰度图片测试和训练的规范写法&#xff1a;封装在函数中 2.展平操作&#xff1a;除第一个维度batchsize外全部展平 3.dropout操作&#xff1a;训练阶段随机丢弃神经元&#xff0c;测试阶段eval模式关闭dropout 作业&#xff1a;仔细学习下测试和训练代…

WordPress_suretriggers 权限绕过漏洞复现(CVE-2025-3102)

免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 前…

基于Spring Boot 电商书城平台系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

LeetCode 39.组合总和:回溯法与剪枝优化的完美结合

一、问题本质与形式化定义 1.1 题目形式化描述 输入&#xff1a;无重复整数数组candidates、目标值target输出&#xff1a;所有和为target的组合集合&#xff0c;满足&#xff1a; 元素可重复使用组合内元素非降序&#xff08;避免重复解&#xff09;解集无重复组合 1.2 问…

windows11安装编译QtMvvm

windows11安装编译QtMvvm 1 从github下载代码2 官方的Download/Installtion3 自行构建编译QtMvvm遇到的问题3.1 `qmake`问题执行命令报错原因分析qmake报错:找不到编译器 cl解决方案3.2 `make qmake_all`问题执行命令报错原因分析make命令未识别解决方案3.3 缺少`perl`问题执行…

unix/linux source 命令,其历史争议、兼容性、生态、未来展望

现在把目光投向unix/linux source命令的历史争议、兼容性、生态和未来展望,这能让我们更全面地理解一个技术点在更广阔的图景中所处的位置。 一、历史争议与设计权衡 虽然 source (或 .) 命令功能强大且不可或缺,但在其发展和使用过程中,也存在一些微妙的争议或设计上的权衡…

开发时如何通过Service暴露应用?ClusterIP、NodePort和LoadBalancer类型的使用场景分别是什么?

一、Service核心概念 Service通过标签选择器&#xff08;Label Selector&#xff09;关联Pod&#xff0c;为动态变化的Pod集合提供稳定的虚拟IP和DNS名称&#xff0c;主要解决&#xff1a; 服务发现负载均衡流量路由 二、Service类型详解 1. ClusterIP&#xff08;默认类型…

从线性代数到线性回归——机器学习视角

真正不懂数学就能理解机器学习其实是个神话。我认为&#xff0c;AI 在商业世界可以不懂数学甚至不懂编程也能应用&#xff0c;但对于技术人员来说&#xff0c;一些基础数学是必须的。本文收集了我认为理解学习本质所必需的数学基础&#xff0c;至少在概念层面要掌握。毕竟&…

华为IP(7)

端口隔离技术 产生的背景 1.以太交换网络中为了实现报文之间的二层隔离&#xff0c;用户通常将不同的端口加入不同的VLAN&#xff0c;实现二层广播域的隔离。 2.大型网络中&#xff0c;业务需求种类繁多&#xff0c;只通过VLAN实现二层隔离&#xff0c;会浪费有限的VLAN资源…

Docker Desktop无法在windows低版本进行安装

问题描述 因工作需要&#xff0c;现在一台低版本的window系统进行Docker Desktop的安装&#xff0c;但是安装过程当中出现了报错信息 系统版本配置 原因分析&#xff1a; 关于本机查看了系统的版本号&#xff0c;版本号如下为1909,但是docker Desktop要求的最低的win10版本…

深入理解 Maven 循环依赖问题及其解决方案

在 Java 开发领域&#xff0c;Maven 作为主流构建工具极大简化了依赖管理和项目构建。然而**循环依赖&#xff08;circular dependency&#xff09;**问题仍是常见挑战&#xff0c;轻则导致构建失败&#xff0c;重则引发类加载异常和系统架构混乱。 本文将从根源分析循环依赖的…

Git 全平台安装指南:从 Linux 到 Windows 的详细教程

目录 一、Git 简介 二、Linux 系统安装指南 1、CentOS/RHEL 系统安装 2、Ubuntu/Debian 系统安装 3、Windows 系统安装 四、安装后配置&#xff08;后面会详细讲解&#xff0c;现在了解即可&#xff09; 五、视频教程参考 一、Git 简介 Git 是一个开源的分布式版本控制系…

微服务-Sentinel

目录 背景 Sentinel使用 Sentinel控制台 Sentinel控制规则 Sentinel整合OpenFeign 背景 在微服务项目架构中&#xff0c;存在多个服务相互调用场景&#xff0c;在某些情况下某个微服务不可用时&#xff0c;上游调用者若一直等待&#xff0c;会产生资源的消耗&#xff0c;极端情…

智慧零工平台前端开发实战:从uni-app到跨平台应用

智慧零工平台前端开发实战:从uni-app到跨平台应用 本文将详细介绍我如何使用uni-app框架开发一个支持微信小程序和H5的零工平台前端应用,包含技术选型、架构设计、核心功能实现及部署经验。 前言 在当今移动互联网时代,跨平台开发已成为提高开发效率的重要手段。本次我选择…

Qt实现csv文件按行读取的方式

Qt实现csv文件按行读取的方式 场景:我有一个保存数据的csv文件,文件内保存的是按照行保存的数据,每行数据是以逗号为分隔符分割的文本数据。如下图所示: 现在,我需要按行把这些数据读取出来。 一、使用QTextStream文本流的方式读取 #include <QFile>void readfil…

day17 leetcode-hot100-34(链表13)

23. 合并 K 个升序链表 - 力扣&#xff08;LeetCode&#xff09; 1.数组排序 思路 &#xff08;1&#xff09;将全部的节点存储到数组中 &#xff08;2&#xff09;对数组进行排序 &#xff08;3&#xff09;最后创建一个全新的链表 具体代码 /*** Definition for singly…