【算法34】蓄水池抽样算法 (Reservoir Sampling Algorithm)
蓄水池抽样算法简介
蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数。其时间复杂度为 O(N),包含下列步骤 (假设有一维数组 S, 长度未知,需要从中随机选择 k 个元素, 数组下标从 1 开始), 伪代码如下:
array R[k]; // result
integer i, j; // fill the reservoir array
for each i in to k do
R[i] := S[i]
done; // replace elements with gradually decreasing probability
for each i in k+ to length(S) do
j := random(, i); // important: inclusive range
if j <= k then
R[j] := S[i]
fi
done
算法首先创建一个长度为 k 的数组(蓄水池)用来存放结果,初始化为 S 的前 k 个元素。然后从 k+1 个元素开始迭代直到数组结束,在 S 的第 i 个元素,算法生成一个随机数 $j \in [1, i]$, 如果 j <= k, 那么蓄水池的第 j 个元素被替换为 S 的第 i 个元素。
算法的正确性证明
定理:该算法保证每个元素以 k / n 的概率被选入蓄水池数组。
证明:首先,对于任意的 i,第 i 个元素进入蓄水池的概率为 k / i;而在蓄水池内每个元素被替换的概率为 1 / k; 因此在第 i 轮第j个元素被替换的概率为 (k / i ) * (1 / k) = 1 / i。 接下来用数学归纳法来证明,当循环结束时每个元素进入蓄水池的概率为 k / n.
假设在 (i-1) 次迭代后,任意一个元素进入 蓄水池的概率为 k / (i-1)。有上面的结论,在第 i 次迭代时,该元素被替换的概率为 1 / i, 那么其不被替换的概率则为 1 - 1/i = (i-1)/i;在第i 此迭代后,该元素在蓄水池内的概率为 k / (i-1) * (i-1)/i = k / i. 归纳部分结束。
因此当循环结束时,每个元素进入蓄水池的概率为 k / n. 命题得证。
算法的C++实现
实现部分比较简单,关键点也有详细的注释,为了验证算法的正确性,对[1,10]的数组,随机选择前五个进行验证,运行10000次后,每个数字出现的频率应该是基本相等的,算法的运行结果也证实了这一点,如下图所示。
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <ctime>
using namespace std; /**
* Reservoir Sampling Algorithm
*
* Description: Randomly choose k elements from n elements ( n usually is large
* enough so that the full n elements may not fill into main memory)
* Parameters:
* v - the original array with n elements
* n - the length of v
* k - the number of choosen elements
*
* Returns:
* An array with k elements choosen from v
*
* Assert:
* k <= n
*
* Author: python27
* Date: 2015/07/14
*/
vector<int> ReservoirSampling(vector<int> v, int n, int k)
{
assert(v.size() == n && k <= n); // init: fill the first k elems into reservoir
vector<int> reservoirArray(v.begin(), v.begin() + k); int i = ;
int j = ;
// start from the (k+1)th element to replace
for (i = k; i < n; ++i)
{
j = rand() % (i + ); // inclusive range [0, i]
if (j < k)
{
reservoirArray[j] = v[i];
}
} return reservoirArray;
} int main()
{
vector<int> v(, );
for (int i = ; i < ; ++i) v[i] = i + ; srand((unsigned int)time(NULL));
// test algorithm RUN_COUNT times
const int RUN_COUNT = ;
int cnt[] = {};
for (int k = ; k <= RUN_COUNT; ++k)
{
cout << "Running Count " << k << endl; vector<int> samples = ReservoirSampling(v, , ); for (size_t i = ; i <samples.size(); ++i)
{
cout << samples[i] << " ";
cnt[samples[i]]++;
}
cout << endl;
} // output frequency stats
cout << "*************************" << endl;
cout << "Frequency Stats" << endl;
for (int num = ; num < ; ++num)
{
cout << num << " : \t" << cnt[num] << endl;
}
cout << "*************************" << endl; return ;
}
运行结果如下:
算法的局限性
蓄水池算法的基本假设是总的样本数很多,不能放入内存,暗示了选择的样本数 k 是一个与 n 无关的常数。然而在实际的应用中,k 常常与 n 相关,比如我们想要随机选择1/3 的样本 (k = n / 3),这时候就需要别的算法或者分布式的算法。
参考文献
【1】 Wikipedia:Reservoir Sampling
【算法34】蓄水池抽样算法 (Reservoir Sampling Algorithm)的更多相关文章
- 【数据结构与算法】蓄水池抽样算法(Reservoir Sampling)
问题描述 给定一个数据流,数据流长度 N 很大,且 N 直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出 m 个不重复的数据. 比较直接的想法是利用随机数算 ...
- 蓄水池抽样算法 Reservoir Sampling
2018-03-05 14:06:40 问题描述:给出一个数据流,这个数据流的长度很大或者未知.并且对该数据流中数据只能访问一次.请写出一个随机选择算法,使得数据流中所有数据被选中的概率相等. 问题求 ...
- Reservoir Sampling - 蓄水池抽样算法&&及相关等概率问题
蓄水池抽样——<编程珠玑>读书笔记 382. Linked List Random Node 398. Random Pick Index 从n个数中随机选取m个 等概率随机函数面试题总结 ...
- leetcode398 and leetcode 382 蓄水池抽样算法
382. 链表随机节点 给定一个单链表,随机选择链表的一个节点,并返回相应的节点值.保证每个节点被选的概率一样. 进阶:如果链表十分大且长度未知,如何解决这个问题?你能否使用常数级空间复杂度实现? 示 ...
- Reservoir Sampling 蓄水池抽样算法,经典抽样
随机读取数据,如何保证真随机是不可能的,因为计算机的随机函数是伪随机的. 但是在不考虑计算机随机函数的情况下,如何保证数据的随机采样呢? 1.系统提供的shuffle函数 C++/Java都提供有sh ...
- 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 ...
- 算法系列:Reservoir Sampling
copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...
随机推荐
- eclispe 通过git向码云上传
本文将介绍如何将本地的项目提交到开源中国上去,过程比较详细,实现起来很简单.由于自己也算是一个新手,所以没有做过多的解释,只是单纯的描述了该如何去做. 1.在开源中国上面新建一个空项目 到这里也就结束 ...
- 元素的定位id和name
1.元素定位: 元素的定位是自动化测试的核心,要想操作一个元素,首先应该识别这个元素 webdriver提供了一系列的元素定位方法,常用的有以下几种 id name class name partia ...
- PIE结对编程
学习进度条 点滴成就 学习时间 新编写代码行数 博客量 学到知识点 第一周 8 0 0 了解软件工程 第二周 7 0 1 了解软件工程 第三周 11 0 1 用例图 第四周 6 25 0 结对编程 第 ...
- 第七章 二叉搜索树(c)平衡与等价
- .NET资源文件实现多语言切换
1.创建对应的资源文件 lang.en.resx 英文 lang.resx 中文,默认 lang.zh-tw.resx 繁体 首先说明,这三个文件前面部分名称需要一样,只是 点 后面的语言代号 ...
- 使用threejs点云秀出酷炫的图片效果(一)
来源:http://blog.csdn.net/srk19960903/article/details/70214556 使用了点云拼凑出了照片轮播十分有趣,于是用threejs实现这个效果. 首先这 ...
- PHP在win7安装Phalcon框架
我的环境是64位的 Win7. 安装 Phalcon 也极其简单,只需要下载一个文件(php_phalcon.dll), 要以 phpinfo() 里面“Architecture”属性为准! 下载地址 ...
- linux下一些常用系统命令
查看系统打开的文件数 lsof|wc -l 查看当前目录下的文件数 find -type f | wc -l 查看某个目录下的文件数,注意这里/home包括其所有子目录 find /home -typ ...
- PAT 1070 结绳(25)(代码)
1070 结绳(25 分) 给定一段一段的绳子,你需要把它们串成一条绳.每次串连的时候,是把两段绳子对折,再如下图所示套接在一起.这样得到的绳子又被当成是另一段绳子,可以再次对折去跟另一段绳子串连.每 ...
- nzhtl1477-ただいま帰りました ( bfs )
nzhtl1477-ただいま帰りました 题目描述 珂学题意: 你是威廉!你要做黄油蛋糕给珂朵莉吃~! 68号岛有n个商店,有的商店直接有小路连接,小路的长度都为1 格里克告诉了你哪些地方可能有做黄油蛋 ...