水塘抽样算法(Reservoir Sampling Algorithm)

文章目录

  • 应用场景
  • 算法步骤
  • 算法原理
  • 代码实现

应用场景

主要用于解决大数据流中的随机抽样问题,即:当内存有限,数据长度很大,甚至未知,那么如何从中随机选取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} 1k+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×(1k+1k×k1)×(1k+2k×k1)××(1n1k×k1)×(1nk×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×(1i+1k×k1)×(1i+2k×k1)××(1n1k×k1)×(1nk×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;
    }

你可能感兴趣的:(算法与数据结构,算法)