蓄水池抽样问题
蓄水池抽样算法是一种抽样算法,对于一个不知道大小的的集合(通常是流式数据),抽取的样本值能够保证随机。
特点:其时间复杂度并不是很高O(n),空间复杂度通常是O(k),能够很大程度地节省内存。
问题:我现在有一个很长并且不知道多长的序列,怎么从中取出k个完全随机的数。
用一个足够大的数组存放所有数据然后随机这种方法肯定是行不通的,因为我们不知道序列有多长,并且计算机并不能给我们开一个所谓“足够大”的数组。
那么蓄水池抽样算法是怎么做的?
首先,我们要维护一个大小为k的数组,存放的就是我们要抽取的那k个数字。先把数据的前k个放到数组中,然后之后的每一个数据我们都需要做一个操作,假设我们现在遍历完第n个数字(现在的n刚好等于k),我们从取一个随机数对n+1取模,如果结果小于k就替换掉数组中相应位置的元素,如果大于等于k的话就放弃这个数。代码如下:
int num, ind = 0;
int arr[k];
while (cin >> num) {
if (ind < k) {
arr[ind++] = num;
continue;
}
int rd = rand() % ind; //这个时候ind就是之前说的n+1
if (rd < k) arr[rd] = num;
ind++;
}
代码其实很简单,但是现在的问题是,为什么这么做可以保证我们取到所有数据的概率都是相等的,接下来我们证明一下。
首先,当我们处理完第n个数时,目前任意一个数字能够存在于数组中的概率都是k/n。当我们处理第n+1个数字时,因为我们要取一个随机数对n+1取模,如果结果小于k就替换掉数组中相应位置的元素,如果大于等于k的话就放弃这个数。所以这个数字在执行完这次操作后能留在数组中的概率为k/n+1,所以,现在我们只需要证明在n-1之前所有数字执行完这次操作后留在数组的概率也是这个,就可以证明每一个数字被抽出的概率都相等。
现在来推理一下这个概率,不难理解,它一定等于这个数字本来就在数组并且第n+1个数没进数组的概率加上这个数字在数组并且第n+1个数进到数组中但是没有替换掉它的概率。可能有点长,但是式子是这样:
k
n
∗
n
+
1
−
k
n
+
1
+
k
n
∗
k
n
+
1
∗
k
−
1
k
\frac{k}{n} * \frac{n+1-k}{n+1} + \frac{k}{n} * \frac{k}{n+1} * \frac{k-1}{k}
nk∗n+1n+1−k+nk∗n+1k∗kk−1
最后化简一下就是:
k
n
+
1
\frac{k}{n+1}
n+1k
刚好和之前计算的第n+1个数字被抽到的概率一样,所以我们就证明了这种做法是可以做到对每一个数据的抽取概率相等的。
应用场景:
蓄水池抽样的应用场景通常来说只是流式数据这种不知道具体有多少的数据。切记并不是“大量的数据”就都用这种方式,这种方式只是非常适用于流式数据。