最近从朋友那里听到了一个算法——水池抽样算法,了解了一下,觉得挺简单有趣的,于是就花了点时间学习了一下。
背景
从总数据中随机抽取k个数,要求每个数被抽到的概率相等。
引入
首先我们来看两道简单概率题,
- 1,袋中放有3个球,红绿蓝,第一次取出一球,不放回,第二次再取一球.则抽到红球的概率是多少?
答案:1-2/3 * 1/2= 2/3
- 2,袋中放有3个球,红绿蓝,第一次取出一球,放回,第二次再取一球.则至少抽到红球的概率是多少?
答案:1-2/3*2/3 = 5/9
(取到一次红球4/9,两次红球1/9,两种情况相加). - 3,从 赤橙黄绿青蓝紫七个球中,取出3个球,取到赤球的概率是多少?
答案:3/7
(第一个就取到 1/7 + 第二个才取到 6/7 * 1/6 + 第三个才取到 6/7 * 5/6 * 1/5 = 3/7)
如果这些概率题能够理解,那么后面水塘抽样的理解就很简单了。
在什么情况下使用?
其实如果知道总数 n,我们直接产生n 内的k个随机数即可,因此用不到水池抽样。
水塘抽样的条件是在,不知道总数量(假设为n)的情况下,通过一次遍历,随机抽出k个数,这个随机指的是每个数被抽到的最终概率为(k/n),遍历的同时进行取样。
例如,要在一个链表中,通过一次遍历,随机取两个数。
或者,在一个极大文本中,随机抽取10行。等等。
思路和代码
思路:首先取1-k
个数放进集合 result
,然后从k+1
开始遍历,在遍历的时候,取k+1
内的一个随机数(假设为j
),如果取到的数j <=k
,那么将这个遍历到的数替换进 result
的第j
个数。 继续下一次遍历直到最后。(体现为代码值-1
)。 代码很简单:
int arr[] = {1,2,3,4,5,6,7}; //假设值,实际应该是一个只可以判断是否到结尾的类集合对象。
int k=5;
int result[] =new int[k];
for(int i=0;i<k;i++){ //初始取值
result[i] = arr[i];
}
int x=k;
while(x<arr.length){ //实际的判断是,arr是否到最后了
int j = new Random().nextInt(x);
if(j<k){
result[j] = arr[x];
}
x++;
}
//运行结束,最终result几位结果。
证明
明白了思路之后,代码很简单。抽样算法的一点难点只有证明部分。通过反向验证,其实也很简单,能够理解前面的概率题即可理解,如下:
- 第1个数被选中,概率为:
1 * [k/(k+1) * (k+1)/(k+2) * ... * (n-1)/n ]= k/n
(第一次被选中 * 后续遍历不被替换的概率) - 第x个数(x 大于k小于n,例如x=k+10)被选中,概率为:
k/(k+10) * (k+10)/(k+11) * ... * (n-1)/n = k/n
(在遍历第 k+10 时抽到的随机值为k内,即被选中,并且后续不被替换) - 第n个数被选中(最后)的概率:
k/n
(在遍历第 n 时抽到的随机值为k内,即被选中)
因此,可知,每一个数被选中的概率都为 k/n