蓄水池抽样算法

问题描述

问题:有一段很长整数流,不知道其长度,内存可能也不够存下所有数据。希望从这个整数流中抽样出k个整数,要求每个整数被抽样的概率相同。

这种问题称之为蓄水池抽样问题。

最大堆解决

如果一个数组是随机排列的,我们取前k个元素也是随机的,那么这k元素可以称之为一个随机抽样。

​ ——非名家之言

依以上所言,我们只需要得到整数流的一个随机排列就可以了。但是我们连长度n都不知道,怎么随机排列?

我们一定要排列好所有元素吗?我们只需要抽样k个,因此我们只需要知道哪k个元素排在前面就可以。这是不是让你想起了这样一个问题:一堆数中取前k个最大的数。这个问题可以用堆解决,同样蓄水池抽样问题也可以用堆解决。

算法描述:维护一个大小为k的大顶堆,每次来一个数,随机生成一个key,如果当前元素的key比堆顶的元素要小,弹出堆顶元素,将当前元素和key加入堆中。最终堆里的元素是一个随机排列(因为key是随机生成的)里最小的前k个数。这k个数就是一个随机抽样。(也可以用小顶堆,那就是取最大的的k个数)

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vector<int> reservoirSamplingByHeap(vector<int> &nums, int k) {
priority_queue< pair<int, int> > heap;
for (int i = 0; i < nums.size(); i++) {
int r = rand(); // 随机生成一个key
if (heap.size() < k) heap.push(make_pair(nums[i], r));
else {
if (heap.top().second > r) {
heap.pop();
heap.push(make_pair(nums[i], r));
}
}
}
vector<int> sample;
while (!heap.empty()) {
sample.push_back(heap.top().first);
heap.pop();
}
return sample;
}

时间复杂度:nlgk

更快的解法

nlgk已经足够快,但是也有更快的解法,也相当巧妙。

算法描述:维护一个大小为k的蓄水池,初始将前k个元素放入蓄水池中,从第k+1个元素开始,每个元素以ki的概率选中,随机替换蓄水池中的一个元素。通过这种方式每个元素被选取到的概率相等。

证明

我们先从最简单的问题入手:k=1

则第i个元素被选取的概率是1i,但是现在被选取了并不代表后来不会被替换掉,所以元素i最终被选取为被选取的概率乘上不被替换的概率:

P(i)=1i×ii+1××n1n=1n

对于k>1,要分两种情况,

  1. 对于i>k,其最终被选中不仅要求扫描到他时被选中,还要求后来者不被选中或即使选中也不会将它替换。因此对于j>i,其不会对i构成威胁的概率是:

    F(j)=kj×k1k+jkj=j1j

    i被选中的概率:

    P(i)=ki×F(i+1)×F(i+2)××F(n) =ki×ii+1×i+1i+2××n1n =kn
  2. 对于i<=k,其最终被选中要求第k个元素后面的元素不被选中,或者选中但不替换到它。对与j>k,其不会对i构成威胁的概率等同于F(j)

    i被选中的概率为:

    P(i)=F(k+1)×F(k+2)×F(k+3)××F(n) =kk+1×k+1k+2××n1n =kn

证毕

代码如下:

1
2
3
4
5
6
7
8
vector<int> reservoirSampling(vector<int> &nums, int k) {
for (int i = k; i < nums.size(); i++) {
int r = rand() % (i + 1);
// k/i 的概率
if (r < k) swap(nums[r], nums[i]);
}
return vector<int>(nums.begin(), nums.begin() + k);
}

分布式抽样

现在数据量太大,不可能所有数据都放在一台服务器上,如果输入放在m台机器上该如何抽样?

不管环境怎么变,问题的本质没有变:产生一个随机的序列,取序列的前k个数。最大堆方法可以适用与这种情况。

算法描述:我们在m台机器上分别用最大堆方法抽样出最大的k个元素,然后将这mk个元素发送到一台机器上再用最大堆方法抽样。

通过这种方法,我们得到的还是随机排列中最小的k个元素,这是一个随机抽样。

更进一步

如果k太大,内存放不下怎么办?

算法描述:可以设置L个桶(桶可以放到外存),每次随机的时候不仅产生一个key,同时产生一个桶号l,将数据分配到l号桶中。默认小号桶比大号桶数据小。统计前j个桶的所有元素的个数S(j),如果j是第一个S(j)>k的桶,在第j个桶中抽样kS(j1)个元素(排个序就行)。因为桶足够多,每个桶中的元素足够少,可以加载到内存。

因为通过排序的关键了(l,r)是随机产生的,该排列也是一个随机排列。