问题描述
问题:有一段很长整数流,不知道其长度,内存可能也不够存下所有数据。希望从这个整数流中抽样出k个整数,要求每个整数被抽样的概率相同。
这种问题称之为蓄水池抽样问题。
最大堆解决
如果一个数组是随机排列的,我们取前k个元素也是随机的,那么这k元素可以称之为一个随机抽样。
——非名家之言
依以上所言,我们只需要得到整数流的一个随机排列就可以了。但是我们连长度n都不知道,怎么随机排列?
我们一定要排列好所有元素吗?我们只需要抽样k个,因此我们只需要知道哪k个元素排在前面就可以。这是不是让你想起了这样一个问题:一堆数中取前k个最大的数。这个问题可以用堆解决,同样蓄水池抽样问题也可以用堆解决。
算法描述:维护一个大小为k的大顶堆,每次来一个数,随机生成一个key
,如果当前元素的key
比堆顶的元素要小,弹出堆顶元素,将当前元素和key
加入堆中。最终堆里的元素是一个随机排列(因为key
是随机生成的)里最小的前k个数。这k个数就是一个随机抽样。(也可以用小顶堆,那就是取最大的的k个数)
代码:
1 | vector<int> reservoirSamplingByHeap(vector<int> &nums, int k) { |
时间复杂度:nlgk
更快的解法
nlgk已经足够快,但是也有更快的解法,也相当巧妙。
算法描述:维护一个大小为k的蓄水池,初始将前k个元素放入蓄水池中,从第k+1个元素开始,每个元素以ki的概率选中,随机替换蓄水池中的一个元素。通过这种方式每个元素被选取到的概率相等。
证明:
我们先从最简单的问题入手:k=1
则第i个元素被选取的概率是1i,但是现在被选取了并不代表后来不会被替换掉,所以元素i最终被选取为被选取的概率乘上不被替换的概率:
P(i)=1i×ii+1×⋯×n−1n=1n对于k>1,要分两种情况,
对于i>k,其最终被选中不仅要求扫描到他时被选中,还要求后来者不被选中或即使选中也不会将它替换。因此对于j>i,其不会对i构成威胁的概率是:
F(j)=kj×k−1k+j−kj=j−1j则i被选中的概率:
P(i)=ki×F(i+1)×F(i+2)×⋯×F(n) =ki×ii+1×i+1i+2×⋯×n−1n =kn对于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×⋯×n−1n =kn
证毕
代码如下:
1 | vector<int> reservoirSampling(vector<int> &nums, int k) { |
分布式抽样
现在数据量太大,不可能所有数据都放在一台服务器上,如果输入放在m台机器上该如何抽样?
不管环境怎么变,问题的本质没有变:产生一个随机的序列,取序列的前k个数。最大堆方法可以适用与这种情况。
算法描述:我们在m台机器上分别用最大堆方法抽样出最大的k个元素,然后将这mk个元素发送到一台机器上再用最大堆方法抽样。
通过这种方法,我们得到的还是随机排列中最小的k个元素,这是一个随机抽样。
更进一步
如果k太大,内存放不下怎么办?
算法描述:可以设置L个桶(桶可以放到外存),每次随机的时候不仅产生一个key
,同时产生一个桶号l,将数据分配到l号桶中。默认小号桶比大号桶数据小。统计前j个桶的所有元素的个数S(j),如果j是第一个S(j)>k的桶,在第j个桶中抽样k−S(j−1)个元素(排个序就行)。因为桶足够多,每个桶中的元素足够少,可以加载到内存。
因为通过排序的关键了(l,r)是随机产生的,该排列也是一个随机排列。