蓄水池采样算法

问题描述分析

采样问题经常会被遇到,比如:

  1. 从 100000 份调查报告中抽取 1000 份进行统计。
  2. 从一本很厚的电话簿中抽取 1000 人进行姓氏统计。
  3. 从 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\) 步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:

\[1 \times \frac{k}{k + 1} \times \frac{k + 1}{k + 2} \times \frac{k + 2}{k + 3} \times … \times \frac{n - 1}{n} = \frac{k}{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}{j} \times \frac{j}{j + 1} \times \frac{j + 1}{j + 2} \times \frac{j + 2}{j + 3} \times ... \times \frac{n - 1}{n} = \frac{k}{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)的更多相关文章

  1. 【算法34】蓄水池抽样算法 (Reservoir Sampling Algorithm)

    蓄水池抽样算法简介 蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数.其时间复杂度为 O(N),包含 ...

  2. 蓄水池抽样算法 Reservoir Sampling

    2018-03-05 14:06:40 问题描述:给出一个数据流,这个数据流的长度很大或者未知.并且对该数据流中数据只能访问一次.请写出一个随机选择算法,使得数据流中所有数据被选中的概率相等. 问题求 ...

  3. Reservoir Sampling 蓄水池采样算法

    https://blog.csdn.net/huagong_adu/article/details/7619665 https://www.jianshu.com/p/63f6cf19923d htt ...

  4. 382. Linked List Random Node(蓄水池采样)

    1. 问题 给定一个单链表,随机返回一个结点,要求每个结点被选中的概率相等. 2. 思路 在一个给定长度的数组中等概率抽取一个数,可以简单用随机函数random.randint(0, n-1)得到索引 ...

  5. 【数据结构与算法】蓄水池抽样算法(Reservoir Sampling)

    问题描述 给定一个数据流,数据流长度 N 很大,且 N 直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出 m 个不重复的数据. 比较直接的想法是利用随机数算 ...

  6. Spark MLlib之水塘抽样算法(Reservoir Sampling)

    1.理解 问题定义可以简化如下:在不知道文件总行数的情况下,如何从文件中随机的抽取一行? 首先想到的是我们做过类似的题目吗?当然,在知道文件行数的情况下,我们可以很容易的用C运行库的rand函数随机的 ...

  7. Reservoir Sampling - 蓄水池抽样问题

    问题起源于编程珠玑Column 12中的题目10,其描述如下: How could you select one of n objects at random, where you see the o ...

  8. Reservoir Sampling - 蓄水池抽样

    问题起源于编程珠玑Column 12中的题目10,其描述如下: How could you select one of n objects at random, where you see the o ...

  9. MCMC等采样算法

    一.直接采样 直接采样的思想是,通过对均匀分布采样,实现对任意分布的采样.因为均匀分布采样好猜,我们想要的分布采样不好采,那就采取一定的策略通过简单采取求复杂采样. 假设y服从某项分布p(y),其累积 ...

随机推荐

  1. 51nod1369 无穷印章

    有一个印章,其完全由线段构成.这些线段的线足够细可以忽略其宽度,就像数学上对线的定义一样,它们没有面积.现在给你一张巨大的白纸(10亿x10亿大小的纸,虽然这个纸很大,但是它的面积毕竟还是有限的),你 ...

  2. java.io.FileNotFoundException:SESSIONS.ser (系统找不到指定的路径。)

    问题如下: java.io.FileNotFoundException: E:\apache-tomcat-8.0.37\work\Catalina\localhost\20161013Shoppin ...

  3. 黄聪:wordpress中PHP运行错最有效解决办法Fatal error: Out of memory (allocated 6029312)(转)

    近日在升级wordpress 3.2.1和若干插件的过程中,发现了一个wordpress的错误:Allowed memory size of XXX bytes exhausted Fatal err ...

  4. 在VS2010上使用C#调用非托管C++生成的DLL文件(图文讲解)

    http://www.cyqdata.com/cnblogs/article-detail-35876#

  5. BIP_开发案例02_BI Publisher中复杂案例实现代码(案例)

    2014-12-27 Created By BaoXinjian

  6. POJ 1066 Treasure Hunt(计算几何)

    题意:给出一个100*100的正方形区域,通过若干连接区域边界的线段将正方形区域分割为多个不规则多边形小区域,然后给出宝藏位置,要求从区域外部开辟到宝藏所在位置的一条路径,使得开辟路径所需要打通的墙壁 ...

  7. [复变函数]第05堂课 1.4 复球面与 $\infty$; 作业讲解; 2 解析函数 2.1 解析函数的概念与 Cauchy-Riemann 方程

    1. 复球面 大漠孤烟直, 长河落日圆. $$\bex \bbC\cong \bbS^2\bs \sed{N},\quad \bbC_\infty=\bbC\cup \sed{\infty}\mbox ...

  8. ALITUM DESIGNER 多PIN脚IC元件封装的制作

    多IC芯片的管教众多,一个一个的添加引脚效率较低,网上有好的方法,现总结如下 1 在元件库.schlib中新建元件,画出框图和添加第一个PIN脚 2利用smart paste快速放置众多PIN脚(具体 ...

  9. Spring MVC 中文乱码的解决

    对于POST方法提交的中文乱码 , 可在web.xml中添加如下代码 : <filter> <filter-name>encodingFilter</filter-nam ...

  10. http://www.imooc.com/video/4767 zepto教学视频笔记

    一.介绍js移动端框架:zepto.js与jquery mobile 对比:zepto特点 1.与jquery相似度95%,会jquery基本会zepto: 2.API少,轻量级框架 3.移动端无缝接 ...