1. 程式人生 > 實用技巧 >洗牌演算法與水塘抽樣

洗牌演算法與水塘抽樣

寫在最前:要注意洗牌演算法與水塘取樣演算法之間的區別
水塘抽樣是一系列的隨機演算法,其目的在於從包含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}\) 的概率保持原有選擇即可