洗牌算法与水塘抽样

写在最前:要注意洗牌算法与水塘采样算法之间的区别
水塘抽样是一系列的随机算法,其目的在于从包含n个项目的集合S中选取k个样本,其中n为一很大或未知的数量,尤其适用于不能把所有n个项目都存放到主内存的情况。
洗牌算法就是将数据完全打乱的一种算法思想,类似于我们打扑克时候的洗牌。

洗牌算法

public void shuffle(int[] arr){
      int n = arr.length;
      Random rdm = new Random();
      for(int i = 0;i<n;i++){
            // [0,n)
            int rand = rdm.nextInt(i,n);
            /**
            错误写法:int rand = rdm.nextInt(0,n);
            这样子的话,总共有n**n种可能,而不是n!
            */
            swap(arr,i,rand);
      }
}
public void swap(int[] arr,int i,int j){
      int tmp = arr[i];
      arr[i] = arr[j];
      arr[j] = tmp;
}

怎么能够说明确实是随机的呢?

首先,完全随机的话,\(n\)个数字的排列应该有\(n!\)个全排列,如果我们能够说明数组中的元素是我们是从这\(n!\)中等概率选择的就可以了。
然后,针对于代码的第6行,产生\([i,n)\)的随机数,然后将产生的随机数加入到已选的数组中,第一次是从\([0,n)\)中选,有\(n\)中可能,第二次是从\([1,n)\)中选,有\(n-1\)种可能,以此类推得证。

水塘抽样

//返回链表中随机的k个节点的值
public int[] getRandom(ListNode head,int k){
      Random rdm = new Random();
      int[] res = new int[k];
      ListNode p = head;
      // 前k个元素先默认选上
      for(int i = 0;i<k && p!=null;i++){
            res[i] = p.val;
            p=p.next;
      }
      int i = k;
      // 循环遍历链表
      while(p!=null){
            // 生成一个[0,i)之间的随机数
            int j = rdm.nextInt(++i);
            // 随机生成的这个数小于k的概率就是k/i
            if(j<k) res[j] = p.val;
            p=p.next;
      }
      return res;
}

当你遇到第\(i\) 个元素时,应该有 \(\frac{1}{i}\) 的概率选择该元素,\(1 -\frac{1}{i}\) 的概率保持原有的选择.
同理,如果要随机选择 \(k\) 个数,只要在第\(i\) 个元素处以 \(\frac{k}{i}\) 的概率选择该元素,以 \(1 - \frac{k}{i}\) 的概率保持原有选择即可

posted @ 2020-12-24 14:20  BOTAK  阅读(92)  评论(0)    收藏  举报