蓄水池采样算法
问题描述分析
采样问题经常会被遇到,比如:
- 从 100000 份调查报告中抽取 1000 份进行统计。
- 从一本很厚的电话簿中抽取 1000 人进行姓氏统计。
- 从 Google 搜索 "Ken Thompson",从中抽取 100 个结果查看哪些是今年的。
这些都是很基本的采用问题。
既然说到采样问题,最重要的就是做到公平,也就是保证每个元素被采样到的概率是相同的。所以可以想到要想实现这样的算法,就需要掷骰子,也就是随机数算法。(这里就不具体讨论随机数算法了,假定我们有了一套很成熟的随机数算法了)
对于第一个问题,还是比较简单,通过算法生成 \([0, 100000 - 1)\) 间的随机数 1000 个,并且保证不重复即可。再取出对应的元素即可。
但是对于第二和第三个问题,就有些不同了,我们不知道数据的整体规模有多大。可能有人会想到,我可以先对数据进行一次遍历,计算出数据的数量 \(N\),然后再按照上述的方法进行采样即可。这当然可以,但是并不好,毕竟这可能需要花上很多时间。也可以尝试估算数据的规模,但是这样得到的采样数据分布可能并不平均。
算法过程
终于要讲到蓄水池采样算法(Reservoir Sampling)了。先说一下算法的过程:
假设数据序列的规模为 \(n\),需要采样的数量的为 \(k\)。
首先构建一个可容纳 \(k\) 个元素的数组,将序列的前 \(k\) 个元素放入数组中。
然后从第 \(k+1\) 个元素开始,以 \(\frac{k}{n}\) 的概率来决定该元素是否被替换到数组中(数组中的元素被替换的概率是相同的)。 当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。
证明过程
对于第 \(i\) 个数(\(i \le k\))。在 \(k\) 步之前,被选中的概率为 \(1\)。当走到第 \(k + 1\) 步时,被 \(k + 1\) 个元素替换的概率 = \(k + 1\) 个元素被选中的概率 * \(i\) 被选中替换的概率,即为 \(\frac{k}{k + 1} \times \frac{1}{k} = \frac{1}{k + 1}\)。则被保留的概率为 \(1 - \frac{1}{k + 1} = \frac{k}{k + 1}\)。依次类推,不被 \(k + 2\) 个元素替换的概率为 \(1 - \frac{k}{k + 2} \times \frac{1}{k} = \frac{k + 1}{k + 2}\)。则运行到第 \(n\) 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:
\]
对于第 \(j\) 个数(\(j > k\))。在第 \(j\) 步被选中的概率为 \(\frac{k}{j}\)。不被 \(j + 1\) 个元素替换的概率为 \(1 - \frac{k}{j + 1} \times \frac{1}{k} = \frac{j}{j + 1}\)。则运行到第 \(n\) 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:
\]
所以对于其中每个元素,被保留的概率都为 \(\frac{k}{n}\).
代码示例
贴出测试用的示例代码(Java 实现):
public class ReservoirSamplingTest {
private int[] pool; // 所有数据
private final int N = 100000; // 数据规模
private Random random = new Random();
@Before
public void setUp() throws Exception {
// 初始化
pool = new int[N];
for (int i = 0; i < N; i++) {
pool[i] = i;
}
}
private int[] sampling(int K) {
int[] result = new int[K];
for (int i = 0; i < K; i++) { // 前 K 个元素直接放入数组中
result[i] = pool[i];
}
for (int i = K; i < N; i++) { // K + 1 个元素开始进行概率采样
int r = random.nextInt(i + 1);
if (r < K) {
result[r] = pool[i];
}
}
return result;
}
@Test
public void test() throws Exception {
for (int i : sampling(100)) {
System.out.println(i);
}
}
}
结果就不贴出来了,毕竟每次运行结果都不一样。
总结
蓄水池算法适用于对一个不清楚规模的数据集进行采样。以前在某个地方看到过一个面试题,说是从一个字符流中进行采样,最后保留 10 个字符,而并不知道这个流什么时候结束,且须保证每个字符被采样到的几率相同。用的就是这个算法。
在高德纳的 TAOCP 中有对于这个算法的描述,可以说这是个很精巧的算法。在看到这个算法实现前,很难想到可以通过这样的一种方式进行采样。
蓄水池采样算法(Reservoir Sampling)的更多相关文章
-
【算法34】蓄水池抽样算法 (Reservoir Sampling Algorithm)
蓄水池抽样算法简介 蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数.其时间复杂度为 O(N),包含 ...
-
蓄水池抽样算法 Reservoir Sampling
2018-03-05 14:06:40 问题描述:给出一个数据流,这个数据流的长度很大或者未知.并且对该数据流中数据只能访问一次.请写出一个随机选择算法,使得数据流中所有数据被选中的概率相等. 问题求 ...
-
Reservoir Sampling 蓄水池采样算法
https://blog.csdn.net/huagong_adu/article/details/7619665 https://www.jianshu.com/p/63f6cf19923d htt ...
-
382. Linked List Random Node(蓄水池采样)
1. 问题 给定一个单链表,随机返回一个结点,要求每个结点被选中的概率相等. 2. 思路 在一个给定长度的数组中等概率抽取一个数,可以简单用随机函数random.randint(0, n-1)得到索引 ...
-
Spark MLlib之水塘抽样算法(Reservoir Sampling)
1.理解 问题定义可以简化如下:在不知道文件总行数的情况下,如何从文件中随机的抽取一行? 首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的rand函数随机的 ...
-
Reservoir Sampling - 蓄水池抽样问题
问题起源于编程珠玑Column 12中的题目10,其描述如下: How could you select one of n objects at random, where you see the o ...
-
Reservoir Sampling - 蓄水池抽样
问题起源于编程珠玑Column 12中的题目10,其描述如下: How could you select one of n objects at random, where you see the o ...
-
MCMC等采样算法
一.直接采样 直接采样的思想是,通过对均匀分布采样,实现对任意分布的采样.因为均匀分布采样好猜,我们想要的分布采样不好采,那就采取一定的策略通过简单采取求复杂采样. 假设y服从某项分布p(y),其累积 ...
-
随机抽样问题(蓄水池问题Reservoir Sampling)
转自:孤影醉残阳 http://hi.baidu.com/siyupy/item/e4bb218fedf4a0864414cfad 随机抽样问题(蓄水池问题Reservoir Sampling) 随即 ...
随机推荐
-
Uva 1629 切蛋糕
题目链接:https://vjudge.net/contest/146179#problem/B 题意:一个矩形蛋糕上有好多个樱桃,现在要做的就是切割最少的距离,切出矩形形状的小蛋糕,让每个蛋糕上都有 ...
-
iOS二维码扫描IOS7系统实现
扫描相关类 二维码扫描需要获取摄像头并读取照片信息,因此我们需要导入系统的AVFoundation框架,创建视频会话.我们需要用到一下几个类: AVCaptureSession 会话对象.此类作为硬件 ...
-
Hihicoder 题目1 : Trie树(字典树,经典题)
题目1 : Trie树 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编 ...
-
PHP5.4连接sqlserver
1.下载微软的php连接驱动:SQLSRV30.EXE(5.4对应,后面的native client要用2012)/SQLSRV20.EXE(5.3对应,native client要用2008)/SQ ...
-
解决mysql导入导出数据乱码问题
最近在linux上面用mysqldump导出数据,放在windows系统中导入就会出现中文乱码,然后就会导致出现: Unknown MySQL server host和Can't connect to ...
-
UIViewController控制器的生命周期
视图控制器就是用来管理iOS程序中的视图,默认一个UIViewController为我们提供了一个视图UIView 我们称为根视图 - (instancetype)init{ if (self = ...
-
linux驱动调试--段错误之oops信息分析
linux驱动调试--段错误之oops信息分析 http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=29401328&id= ...
-
从mysql数据库取一条记录里的某个字段的值
<?php $link = mysqli_connect("localhost","root","root","dbname ...
-
mysql修改用户密码命令
C:\Users\20160216>mysql -h 10.180.6.183 -u root -pEnter password: ******Welcome to the MySQL moni ...
-
常见的CSS Hack
原文地址: 小昱博客 - 常见的CSS Hack 转载请注明出处,谢谢! 什么是CSS hack 由于不同厂商的流览器或某浏览器的不同版本(如IE6-IE11,Firefox/Safari/Opera ...