应用场景
主要用于解决大数据流中的随机抽样问题,即:当内存有限,数据长度很大,甚至未知,那么如何从中随机选取k个数据,并且要求是等概率。
算法步骤
水塘抽样的步骤是,只遍历一次,每次都考虑一个问题:当前元素是否被选中,选中后替换之前选中的哪一个元素。
采样过程:
step1:首先将前k个元素全部选取。
step2:对于第i个元素(i>k),以概率k/i来决定是否保留该元素,如果保留该元素的话,则随机丢弃掉原有的k个元素中的一个(即原来某个元素被丢掉的概率是1/k)。
算法原理
假设我们要遍历的数据规模为 n n n。
(1)首先,我们来考虑前 k k k个元素:
遍历前
k
k
k个元素时,我们以
p
=
1
p=1
p=1的概率将该元素(或者与该元素相关联的信息,例如索引)加入result。当遍历到
k
+
1
k+1
k+1个元素时,第
k
+
1
k+1
k+1个元素被保留的概率是
k
/
(
k
+
1
)
k/(k+1)
k/(k+1),对于此时result中的元素,它们任何一个被剔除的概率是
1
/
k
1/k
1/k,所以,对于前
k
k
k个元素,它们在这一次遍历中被剔除的概率为
k
k
+
1
×
1
k
\frac{k}{{k + 1}} \times \frac{1}{k}
k+1k×k1
相对应的,对于前
k
k
k个元素中的任一个,它们在第
k
+
1
k+1
k+1次遍历中被保留的概率为
1
−
k
k
+
1
×
1
k
1 - \frac{k}{{k + 1}} \times \frac{1}{k}
1−k+1k×k1
所以,当遍历到第
n
n
n个元素时,前
k
k
k个元素中的任一个被保留的概率为
1
×
(
1
−
k
k
+
1
×
1
k
)
×
(
1
−
k
k
+
2
×
1
k
)
×
⋯
×
(
1
−
k
n
−
1
×
1
k
)
×
(
1
−
k
n
×
1
k
)
=
k
n
1 \times (1 - \frac{k}{{k + 1}} \times \frac{1}{k}) \times (1 - \frac{k}{{k + 2}} \times \frac{1}{k}) \times \cdots \times (1 - \frac{k}{{n - 1}} \times \frac{1}{k}) \times (1 - \frac{k}{n} \times \frac{1}{k})= \frac{k}{n}
1×(1−k+1k×k1)×(1−k+2k×k1)×⋯×(1−n−1k×k1)×(1−nk×k1)=nk
(2)其次,我们来考虑前 k k k个元素之后的所有元素:
对于第i个元素(i>k),以概率k/i来决定是否保留该元素。并且在之后的每一次遍历中,该元素被剔除的概率都为
k
i
+
m
×
1
k
\frac{k}{{i+m}} \times \frac{1}{k}
i+mk×k1
m
m
m表示该次遍历为第i个元素之后的第
m
m
m次遍历
所以,第i个元素(i>k)最终被保留的概率为
k
i
×
(
1
−
k
i
+
1
×
1
k
)
×
(
1
−
k
i
+
2
×
1
k
)
×
⋯
×
(
1
−
k
n
−
1
×
1
k
)
×
(
1
−
k
n
×
1
k
)
=
k
n
\frac{k}{i} \times (1 - \frac{k}{{i + 1}} \times \frac{1}{k}) \times (1 - \frac{k}{{i + 2}} \times \frac{1}{k}) \times \cdots \times (1 - \frac{k}{{n - 1}} \times \frac{1}{k}) \times (1 - \frac{k}{n} \times \frac{1}{k})= \frac{k}{n}
ik×(1−i+1k×k1)×(1−i+2k×k1)×⋯×(1−n−1k×k1)×(1−nk×k1)=nk
这种随机采样方法不需要事先知道问题的规模大小,当遍历到第m(m>k)个元素时,前m个元素中的任一个被保留的概率均为 k / m k/m k/m,当遍历到第n(n>k)个元素时,前n个元素中的任一个被保留的概率均为 k / n k/n k/n。
一般的随机抽样方法需要事先知道数据规模,再用随机数从n个元素中随机抽样k个。但当数据规模特别大且难以用现有的存储空间保存它时,我们无法事先知道数据规模,这时水塘抽样算法就很适合解决这种问题。
代码实现
// 从长度为n的nums数组中随机选取k个数(水塘抽样)
public int[] Reservoir(int[] nums,int k){
int[] res=new int[k];
Random random=new Random();
for(int i=0;i<nums.length;i++){
//前k个数直接加入res
if(i<k) res[i]=i;
//对于第i个数,以k/i的概率让它留下来
else if(random.nextInt(i+1)<k)
//从现有res中随机选一个数踢出去,用留下来的数替换它
res[random.nextInt(k)]=i;
}
return res;
}