1简介
蓄水池抽样算法(Reservoir Sampling)是一种在数据流长度未知且内存有限的情况下,从数据流中随机抽取k个不重复样本的高效算法。
蓄水池抽样算法的核心在于它的效率和随机性。它通过一次遍历数据流来实现抽样,时间复杂度为O(N),空间复杂度为O(k)。这使得它非常适合处理大规模数据流或实时数据。蓄水池抽样算法不仅在理论上优雅,而且在实践中也非常有用。它在数据库抽样、数据流挖掘、机器学习的数据预处理等多个领域都有广泛应用。
2 步骤
- 初始化一个大小为k的空样本集,称为蓄水池(reservoir)。
- 对于输入序列中的每个元素x,执行以下操作: a. 如果蓄水池尚未满(即蓄水池中的元素数量小于k),则将x添加到蓄水池中。 b. 如果蓄水池已满(即蓄水池中的元素数量等于k),则以k/n的概率保留x,其中n是当前已经处理过的元素数量(包括x)。如果保留x,则从蓄水池中随机移除一个元素,并将x添加到蓄水池中。
- 继续处理输入序列中的下一个元素,直到处理完所有元素。
- 最后,蓄水池中的元素就是从输入序列中随机选择的k个样本。
3 原理解释
3.1 P(1) ~ P(5)
由于1~5一开始就在采样池中,P(1)~P(5) 概率算法一样,以1为例:
- 进入采样池中的概率为1。
- 被6替换的概率为1/6,被保留的概率就是5/6。
- 以此类推。
故
3.2 P(6) ~ P(10)
对于一开始不在采样池中的数,以6为例:
- 由于要生成0~6的随机数,要进入采样池,那概数就得小于5,故进入采样池的概率为5/6。
- 被后续数字7替换的概率为1/7,被保留的概率就是6/7。
- 以此类推。
故
3.2 P(n)
4 代码实现
import java.util.Random;
public class ReservoirSampling {
private int[] reservoir;
private Random random;
public ReservoirSampling(int k) {
reservoir = new int[k];
random = new Random();
}
public void add(int value) {
int i = random.nextInt(reservoir.length + 1);
if (i < reservoir.length) {
reservoir[i] = value;
}
}
public int[] getSamples() {
return reservoir;
}
public static void main(String[] args) {
int k = 5; // 抽样数量
int streamLength = 100; // 数据流长度
ReservoirSampling reservoirSampling = new ReservoirSampling(k);
// 模拟数据流
for (int i = 0; i < streamLength; i++) {
reservoirSampling.add(i);
}
// 输出抽样结果
int[] samples = reservoirSampling.getSamples();
System.out.print("抽样结果: ");
for (int sample : samples) {
System.out.print(sample + " ");
}
}
}
5 优缺点
5.1优点
内存效率:蓄水池抽样算法不需要一次性将所有数据加载到内存中,而是逐个处理数据元素。这使得它非常适合于处理大型数据集或数据流,尤其是当内存有限时。
简单性:算法实现简单,逻辑清晰,容易理解和实现。
均匀分布:每个元素被选中的概率是相等的,即每个元素被选中的概率都是k/n,其中k是样本集的大小,n是输入序列的长度。这保证了抽样的代表性和公平性。
在线处理能力:算法可以实时处理数据流,无需等待所有数据都可用,这对于需要快速响应的应用场景非常有用。
5.2 缺点
替换策略:当蓄水池满后,每次新元素进入都可能导致旧元素的替换,这可能会引入一些额外的随机性,尤其是在样本集大小固定且较小的情况下。
初始样本可能不具代表性:由于算法在初期阶段会直接将前k个元素放入蓄水池,如果这些元素不具代表性,可能会导致最终的样本集也不具代表性。
适应性问题:算法一旦确定了样本集大小k,就不易于调整。如果需要动态改变样本集的大小,可能需要重新运行整个算法。
6 使用场景
- 处理大型数据集,尤其是当内存有限时。
- 数据流抽样,例如网络数据包分析等。
- 任何需要从大量数据中随机抽取样本的场景。
7 注意事项
- 确保随机数生成器的质量和正确性。
- 考虑样本集大小的选择,以及如何根据应用场景调整大小。
- 在实现时注意代码的优化,以提高处理速度和效率。
8 练习
给你一个可能含有 重复元素 的整数数组
nums
,请你随机输出给定的目标数字target
的索引。你可以假设给定的数字一定存在于数组中。实现
Solution
类:
Solution(int[] nums)
用数组nums
初始化对象。int pick(int target)
从nums
中选出一个满足nums[i] == target
的随机索引i
。如果存在多个有效的索引,则每个索引的返回概率应当相等。示例:
输入 ["Solution", "pick", "pick", "pick"] [[[1, 2, 3, 3, 3]], [3], [1], [3]] 输出 [null, 4, 0, 2] 解释 Solution solution = new Solution([1, 2, 3, 3, 3]); solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。 solution.pick(1); // 返回 0 。因为只有 nums[0] 等于 1 。 solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。提示:
1 <= nums.length <= 2 * 104
-231 <= nums[i] <= 231 - 1
target
是nums
中的一个整数- 最多调用
pick
函数104
次
class Solution {
Map<Integer, List<Integer>> map = new HashMap<>();
public Solution(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
List<Integer> list = map.getOrDefault(nums[i], new ArrayList<>());
list.add(i);
map.put(nums[i], list);
}
}
public int pick(int target) {
Random random = new Random();
List<Integer> list = map.get(target);
return list.get(random.nextInt(list.size()));
}
}
9 总结
总的来说,蓄水池抽样算法是一种非常实用的随机抽样技术,尤其适用于资源受限和数据量大的场景。尽管存在一些局限性,但它的优点通常远远超过其缺点,使其成为处理大规模数据集时的一个很好的选择。