【数据结构与算法】蓄水池抽样算法(Reservoir Sampling)
问题描述
给定一个数据流,数据流长度 N 很大,且 N 直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出 m 个不重复的数据。
比较直接的想法是利用随机数算法,求 random(N) 得到随机数,但是题目表明数据流极大,这种大数据量是无法一次都读到内存的,这就意味着不能像数组一样根据索引获取元素。获取 N 只能对所有数据进行遍历,耗费时间较大,并且题目强调只能遍历一遍,意味着不能先获取到 N ,那么采用分块存储数据的方法也不可取(遍历不止一遍);如果采用估算,可能导致采样数据不平均。
蓄水池抽样算法
假设数据序列的规模为 n(蓄水池大小),需要采样的数量的为 k。
首先构建一个可容纳 k 个元素的数组,将序列的前 k 个元素放入数组中。
然后从第 k+1 个元素开始,以 k/n 的概率(n 为当前索引位置)来决定该元素是否被替换到数组中(数组中的元素被替换的概率是相同的)。当遍历完所有元素之后,数组中剩下的元素即为所需采取的样本。
证明
- 对于第
i个元素(i <= k),该元素被选中的概率为1,且索引idx <= k时,该元素被替换的概率为0,当索引idx走到k + 1时,第k + 1个元素被选中进行替换的概率为 $ \frac{\mathrm{k}}{\mathrm{k}+1}$ ,第i个元素被选中的概率为 $ \frac{\mathrm{1}}{\mathrm{k}}$,于是第i个元素被第k + 1个元素替换的概率为 $ \frac{\mathrm{k}}{\mathrm{k}+1}$ * $ \frac{\mathrm{1}}{\mathrm{k}}$ = $ \frac{\mathrm{1}}{\mathrm{k}+1}$ ,则第i个元素不被替换的概率为 1 - $ \frac{\mathrm{1}}{\mathrm{k}+1}$ = $ \frac{\mathrm{k}}{\mathrm{k}+1}$ ,同理,第k + 2个元素被选中进行替换的概率为 $ \frac{\mathrm{k}}{\mathrm{k}+2}$ , 第i个元素被选中的概率为 $ \frac{\mathrm{1}}{\mathrm{k}}$ ,于是第i个元素被第k + 2个元素替换的概率为 $ \frac{\mathrm{k}}{\mathrm{k}+2}$ * $ \frac{\mathrm{1}}{\mathrm{k}}$ = $ \frac{\mathrm{1}}{\mathrm{k}+2}$ ,第i个元素不被替换的概率为 1 - $ \frac{\mathrm{1}}{\mathrm{k}+2}$ = $ \frac{\mathrm{k}+1}{\mathrm{k}+2} $。以此类推,运行到第n步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:
\]
- 对于第
j个元素(j > k),该元素被选中的概率为 $ \frac{\mathrm{k}}{\mathrm{j}}$ ,第j + 1个元素被选中进行替换的概率为 $ \frac{\mathrm{k}}{\mathrm{j}+1}$,第j个元素被选中的概率为 $ \frac{\mathrm{1}}{\mathrm{k}}$,第j个元素被替换的概率为 $ \frac{\mathrm{k}}{\mathrm{j}+1}$ * $ \frac{\mathrm{1}}{\mathrm{k}}$ = $ \frac{\mathrm{1}}{\mathrm{j}+1}$,则第 j 个元素不被替换的概率为 1 - $ \frac{\mathrm{1}}{\mathrm{j}+1}$ = $ \frac{\mathrm{j}}{\mathrm{j}+1}$。则运行到第n步时,被保留的概率 = 被选中的概率 * 不被替换的概率,即:
\]
所以对于其中每个元素,被保留的概率都为 $ \frac{\mathrm{k}}{\mathrm{n}}$
代码实现
public class ReservoirSampling {
private int[] pool; // 蓄水池,包含所有数据
private int size; // 蓄水池规格
private Random random;
public ReservoirSampling(int size) {
this.size = size;
random = new Random();
// 初始化数据
pool = new int[size];
for (int i = 0; i < size; i++) {
pool[i] = i;
}
}
public 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 < size; i++) { // K + 1 个元素开始进行概率采样
int r = random.nextInt(i + 1); // 索引下标为 i 个数据时第 i + 1 个数据,r = [0,i]
if (r < K) { // 选中概率为 k/i+1
result[r] = pool[i];
}
}
return result;
}
}
测试
public static void main(String[] args) {
ReservoirSampling test = new ReservoirSampling(1000);
int[] sampling = test.sampling(5);
for (int i : sampling) {
System.out.print(i + " ");
}
}
// 输出 205 907 986 696 443,每次运行结果不同
题目
LeetCode 382. 链表随机节点

给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 被选中的概率一样 。
实现 Solution 类:
Solution(ListNode head) 使用整数数组初始化对象。
int getRandom() 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。
示例:
输入
["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"]
[[[1, 2, 3]], [], [], [], [], []]
输出
[null, 1, 3, 2, 2, 3]
解释
Solution solution = new Solution([1, 2, 3]);
solution.getRandom(); // 返回 1
solution.getRandom(); // 返回 3
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 3
// getRandom() 方法应随机返回 1、2、3中的一个,每个元素被返回的概率相等。
提示:
链表中的节点数在范围 [1, 104] 内
-104 <= Node.val <= 104
至多调用 getRandom 方法 104 次
进阶:
如果链表非常大且长度未知,该怎么处理?
你能否在不使用额外空间的情况下解决此问题?
解:典型的蓄水池算法,当 k 取 1 时的特殊情况,每次只取出一个元素。
class Solution {
ListNode head;
Random random = new Random();
public Solution(ListNode _head) {
this.head = _head;
}
// 另第 idx 个结点被选中的概率为 1/idx ,则该结点不被后面结点覆盖的概率为 1/idx *
// (1 - 1/(idx+1)) * (1 - 1/(idx+2)) * ...* (1 - 1/n) = 1/n
// 白话:对第 idx 个结点计算概率,random = [0, idx), 则random = 0 的概率为 1/idx
// 只要第 idx 个结点的 random 为 0 则选中覆盖原答案,直到选到最后一个结点。
public int getRandom() {
int idx = 1;
ListNode node = head;
int ans = node.val;
while(node != null) {
if(random.nextInt(idx) == 0) ans = node.val;
node = node.next;
idx++;
}
return ans;
}
}
LeetCode 398. 随机数索引
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。
示例:
int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
solution.pick(3);
// pick(1) 应该返回 0。因为只有nums[0]等于1。
solution.pick(1);
解:只需要考虑给定数字即可,对遍历到的给定数字进行编号(1,2,...),再按照蓄水池算法随机取出一个即可
class Solution {
private int[] nums;
public Solution(int[] nums) {
this.nums = nums;
}
public int pick(int target) {
int ans = 0;
int idx = 0;
Random random = new Random();
for(int i = 0; i < nums.length; i++) {
if(nums[i] == target) {
idx++;
if(random.nextInt(idx) == 0) ans = i;
}
}
return ans;
}
}
参考资料
蓄水池抽样算法(Reservoir Sampling)
蓄水池采样算法
挺有意思的一个视频
【数据结构与算法】蓄水池抽样算法(Reservoir Sampling)的更多相关文章
- 【算法34】蓄水池抽样算法 (Reservoir Sampling Algorithm)
蓄水池抽样算法简介 蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数.其时间复杂度为 O(N),包含 ...
- 蓄水池抽样算法 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/ 欢迎转载,请保留此版权声明. -- ...
随机推荐
- Table.Group分组…Group(Power Query 之 M 语言)
数据源: 10列55行数据,其中包括含有重复项的"部门"列和可求和的"金额"列. 目标: 按"部门"列进行分组,显示各部门金额小计. 操作过 ...
- java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 使用过程 和servlet差不多
java nio 写一个完整的http服务器 支持文件上传 chunk传输 gzip 压缩 也仿照着 netty处理了NIO的空轮询BUG 本项目并不复杂 代码不多 ...
- CF675A Infinite Sequence 题解
Content 给定三个整数 \(a,b,c\),问你 \(b\) 是否在以 \(a\) 为首项,公差为 \(c\) 的等差数列中. 数据范围:\(-10^9\leqslant a,b,c\leqsl ...
- CF1494A ABC String 题解
Content 给定 \(T\) 个仅包含大写字母 A,B,C 的字符串 \(s\).问你是否能够通过将每个 A,B,C 换成 (,) 中的一个(同一个字母必须要换成同一个字符),使得最后得到的括号序 ...
- RegExp正则表达式(三)–js中正则表达式的定义
在js中,RegExp正则表达式的定义有两种方式:一种是普通方式,另一种是构造函数方式.无论是那种定义正则表达式的方式,它们都会返回RegExp对象. 普通方式定义正则表达式的格式 语法: var 变 ...
- tomcat下部署两个工程时,只有一个可以访问,另一个出现404错误,该如何解决
tomcat下部署两个工程时,只有一个可以访问,另一个出现404错误,该如何解决 在开发新项目的时候,有时候为了省时,直接把曾经做过的项目工程A拷贝成改名为B工程,然后再在B工程上进行功能的开发, 此 ...
- SpringCloud(三) Zuul
Zuul 有了eureka . feign 和 hystrix 后,基本上就搭建了简易版的分布式项目,但仍存在一些问题,比如: 1.如果我们的微服务中有很多个独立服务都要对外提供服务,那么我们要如何去 ...
- 使用.NET 6开发TodoList应用(8)——实现全局异常处理
系列导航 使用.NET 6开发TodoList应用文章索引 需求 因为在项目中,会有各种各样的领域异常或系统异常被抛出来,那么在Controller里就需要进行完整的try-catch捕获,并根据是否 ...
- The Luckiest number(hdu2462)
The Luckiest number Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Othe ...
- P1629八
P1629八 Accepted 标签:[显示标签] 描述 八是个很有趣的数字啊.八=发,八八=爸爸,88=拜拜.当然最有趣的还是8用二进制表示是1000.怎么样,有趣吧.当然题目和这些都没有关 ...