蓄水池抽样算法

蓄水池抽样算法

蓄水池抽样算法

给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据。

这个场景强调了3件事:

  1. 数据流长度N很大且不可知,所以不能一次性存入内存。
  2. 时间复杂度为O(N)。
  3. 随机选取m个数,每个数被选中的概率为m/N。

算法思路大致如下:

  1. 如果接收的数据量小于m,则依次放入蓄水池。
  2. 当接收到第i个数据时,i >= m,在[0, i]范围内取以随机数d,若d的落在[0, m-1]范围内,则用接收到的第i个数据替换蓄水池中的第d个数据。
  3. 重复步骤2。

算法的精妙之处在于:当处理完所有的数据时,蓄水池中的每个数据都是以m/N的概率获得的。

下面用白话文推导验证该算法。假设数据开始编号为1.

第i个接收到的数据最后能够留在蓄水池中的概率=第i个数据进入过蓄水池的概率*之后第i个数据不被替换的概率(第i+1到第N次处理数据都不会被替换)

  1. 当i<=m时,数据直接放进蓄水池,所以第i个数据进入过蓄水池的概率=1。
  2. 当i>m时,在[1,i]内选取随机数d,如果d<=m,则使用第i个数据替换蓄水池中第d个数据,因此第i个数据进入过蓄水池的概率=m/i。
  3. 当i<=m时,程序从接收到第m+1个数据时开始执行替换操作,第m+1次处理会替换池中数据的为m/(m+1),会替换掉第i个数据的概率为1/m,则第m+1次处理替换掉第i个数据的概率为(m/(m+1))(1/m)=1/(m+1),不被替换的概率为1-1/(m+1)=m/(m+1)。依次,第m+2次处理不替换掉第i个数据概率为(m+1)/(m+2)…第N次处理不替换掉第i个数据的概率为(N-1)/N。所以,之后第i个数据不被替换的概率=m/(m+1)(m+1)/(m+2)(N-1)/N=m/N。
  4. 当i>m时,程序从接收到第i+1个数据时开始有可能替换第i个数据。则参考上述第3点,之后第i个数据不被替换的概率=i/N。
  5. 结合第1点和第3点可知,当i<=m时,第i个接收到的数据最后留在蓄水池中的概率=1m/N=m/N。结合第2点和第4点可知,当i>m时,第i个接收到的数据留在蓄水池中的概率=m/ii/N=m/N。综上可知,每个数据最后被选中留在蓄水池中的概率为m/N。

这个算法建立在统计学基础上,很巧妙地获得了“m/N”这个概率。
代码示例:

vector<int> ReservoirSampling(vector<int> v, int n, int k)
{
     assert(v.size() == n && k <= n);
     // init: fill the first k elems into reservoir
     vector<int> res(v.begin(), v.begin() + k);
     int i = 0,j=0;
     // start from the (k+1)th element to replace
     for (i = k; i < n; ++i)
     {
         j = rand() % (i + 1); // inclusive range [0, i]
         if (j < k)
         {
             res[j] = v[i];
         }
     }
     return res;
}

leetcode例题
398.随机数索引
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。

示例:
int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);
// pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。
solution.pick(3);
// pick(1) 应该返回 0。因为只有nums[0]等于1。
solution.pick(1);
class Solution {
public:
    vector<int> res;
    Solution(vector<int>& nums) {
       res = nums; 
    }
    
    int pick(int target) {
        int c = 0;
        int index = 0;
        for(int i = 0;i < res.size();i++)
            if(res[i] == target){
                c++;
                if(rand() % c == 0) index = i;
            }
        return index;
    }
};

蓄水池抽样算法的典型应用

给定一个长度为N且没有重复元素的数组arr和一个整数M,实现函数等概率随机打印arr中的M个数。

首先在0~n-1中随机得到一个位置a,打印arr[a],然后把arr[a]和数组最后位置的arr[n-1]交换;
然后在0~n-2中随机得到一个位置b,然后把arr[b]和数组最后位置的arr[n-2]交换,

arr:{0,1,2,3,…,b,…,n-2},n-1,

依此类推,直到打印m个数即可。

洗牌算法

核心思想:

  1. 随机选择下标索引,
  2. 交换
void shuffle(vector<int>& data){
    int n=data.size();
    for(int i=n-1;i>=0;i++){
        int randIndex=rand(0,i);
        swap(data[i],data[randIndex];
    }
}

分析洗牌算法正确性的准则: 产⽣的结果必须有 n! 种可能,否则就是错误的。 这个很好解释, 因为⼀个⻓度为 n 的数组的全排列就有 n! 种, 也就是说打乱结果总共有 n! 种

其他

从一个长度为N且没有重复元素的数组arr中随机的取k个数的其他方法

给每⼀个元素关联⼀个随机数, 然后把每个元素插⼊⼀个容量为 k 的⼆叉堆(优先级队列) 按照配对的随机数进⾏排序, 最后剩下的 k 个元素也是随机的。
这个⽅案看起来似乎有点多此⼀举, 因为插⼊⼆叉堆需要 O(logk) 的时间复杂度, 所以整个抽样算法就需要 O(nlogk) 的复杂度, 还不如我们最开始的算法。 但是, 这种思路可以指导我们解决加权随机抽样算法, 权重越⾼, 被随机选中的概率相应增⼤, 这种情况在现实⽣活中是很常⻅的, ⽐如你不往游戏⾥充钱, 就永远抽不到⽪肤

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值