蓄水池算法

137 阅读2分钟

引言

今天刷到一到特别有意思的算法题,在这里记录下;

在数据流场景中,我们希望随机取k个数,但是此时,我们无法确认数据总数N为多少,这个时候该怎么做呢?蓄水池算法就是解决这个问题的。我们针对于k = 1k > 1来分析这个问题;

首先说结论,后面再通过数学归纳法来证明该问题;

论点一:当k=1时,对于第n个数,只需要保证被选中的概率为1/n即可;

论点二:当k>1时,为了保证随机选择k个数,只需要保证对于每个新数,被选中的概率为k/n,并且以1/k的概率和前面被选的k个数互换;

证明

下面是以数学归纳法,证明该问题:

k = 1

  • 对于第一个数,被获取到的概率为1;
  • 对于第二个数,1/2
  • 对于第三个数:前面两个数被留下来的概率 = 当前数不被选中 * 前面某个数被选中 =
  • ....
  • 所以,我们只需要,保证当前取数的时候,被选中的概率为

大致代码如下:

func randomOne(nums []int) int {
    // 这里的n无法确定
    cnt := 0
    ans := 0
    for _, num := range nums {
        cnt++
        if rand.Intn(cnt) == 0 {
            ans = num
        }
    }

    return ans
}

k > 1

  • k个数,被选中的概率为1
  • 个数,那么前面的数被保留的概率 = 当前数不被选中 + 当前数被选中 * 前k个数中被选中 = (假设k=1000
  • ....
  • 综上,我们只需要保证,对于新数n,被选中的概率为k / n即可。如果被选中,将其与前面k个数中随便一个进行替换;
func randomK(nums []int, k int) []int {
   res := []int{}
   for i := 0; i < k; i++ {
      res = append(res, nums[i])
   }

   cnt := k 
   for i := k; i < len(nums); i++ {
      cnt++
      r := rand.Intn(cnt) // 【0, cnt-1】在这个区间取值
      if r < k {
         res[r] = nums[i]
      }
   }

   return res
}

参考