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