水塘抽样算法

水塘抽样算法是用于解决,对于一个未知长度的数据流进行随机采样的问题的。本文介绍几个算法及其变形。
说明:伪代码中S代表未知长度数据流,n为S的实际长度,S.CURRENT表示S当前值,S.NEXT代表S指针下移,R代表结果,k表示结果需要保留的个数。

简单抽样一个元素

Sample(S[1..n],R[1]) //s是未知长度的数据流
  R[1]=S[1]          //取第一个值,初始化结果
  count := 1         //计数器,记录当前为数据流中的第几位
  while S has data
	count++
    j := random(0, 1)   //根据当前位置生成随机数[0,1)
	if j < 1/count
		R[1] = S.CURRENT
	S.NEXT

证明:
一个元素的时候:被抽中的概率为1
两个元素的时候:第一个元素留下的概率为1-1/2,第二个元素留下的概率为 1/2
三个元素的时候:第一个元素留下的概率为(1-1/2)*(1-1/3)=1/3,第二个元素留下的概率为1/2*(1-1/3)=1/3,第三个元素留下的概率为1/3
以此类推,当判断到第i个元素时,第m(0<m<=i)个元素留下的概率为1/m*(1-1/(m+1))*(1-1/(m+2))...(1-1/(i))=1/i就可以保证所有元素的概率相等

算法R

ReservoirSample(S[1..n], R[1..k])
  //使用前l个值初始化结果
  for i = 1 to k
      R[i] := S[i]

  while S has next
	i++
    j := random(1, i)   //取1~i的随机数
    if j <= k
        R[j] := S[i]	//如果位置落在k之内则替换掉对应的R[j]

证明:
第m轮时,R中的每个元素被替换的概率为1/k*k/m=1/m,i能替换掉R中值的概率为k/m,所以到第i轮,第m(0<m<=i)个元素是k/m*(1-1/(m+1))*(1-1/(m+2))...*(1-1/i)=k/i

随机值排序水塘算法

ReservoirSample(S[1..?], R[1..k])
  H = new max-priority-queue	//有序队列
  while S has data
    r = Random(0,1)
    if H.Count < k
      H.Insert(r, S.Current)
    else
      if H.Maximum > r
        H.Extract-Max()
        H.Insert(r, S.Current)
    S.Next

该算法是对每个值进行随机值计算,并保留最大的k个随机值

带权重的随机值排序水塘算法

有些情况,采样的数据是有权重的,下面介绍几个带权重的水塘抽样算法

A-Res算法

ReservoirSample(S[1..?], R[1..k])
  H = new min-priority-queue
  while S has data
    r = Random(0,1) ^ (1/S.Weight)  // 随机值取0-1随机值的1/weight的次方
    if H.Count < k
      H.Insert(r, S.Current)
    else
      if H.Minimum < r		//如果比队列中最小值大,则替换最小值
        H.Extract-Min()	
        H.Insert(r, S.Current)
    S.Next

该算法的随机值是使用了生成随机值的1/weight次方,可以增加高权重的随机值

分布式随机抽样

有些情况需要在分布式的情况下进行随机抽样,可以采用A-Res算法在分布式机器上运行,最后使用各机器上的值进行一起排序达到随机抽样的效果。

洗牌算法Fisher-Yates算法

一种需求是需要对所有的值进行乱序排序

shuffle(S[1..?])
	for(i:= 1 .. n-1)
		r:= random(0,i)		//[0,i]的随机值
		swap(a[r],a[i])

JDK中的Collections.shuffle也是使用这个算法进行数组乱序的。

参考:https://en.wikipedia.org/wiki/Reservoir_sampling