labuladong的算法小抄pdf_随机算法:水塘抽样算法
讀完本文,你可以去力扣拿下如下題目:
382.鏈表隨機節(jié)點
398.隨機數(shù)索引
-----------我最近在 LeetCode 上做到兩道非常有意思的題目,382 和 398 題,關(guān)于水塘抽樣算法(Reservoir Sampling),本質(zhì)上是一種隨機概率算法,解法應(yīng)該說會者不難,難者不會。
我第一次見到這個算法問題是谷歌的一道算法題:給你一個未知長度的鏈表,請你設(shè)計一個算法,只能遍歷一次,隨機地返回鏈表中的一個節(jié)點。
這里說的隨機是均勻隨機(uniform random),也就是說,如果有 n 個元素,每個元素被選中的概率都是 1/n,不可以有統(tǒng)計意義上的偏差。
一般的想法就是,我先遍歷一遍鏈表,得到鏈表的總長度 n,再生成一個 [1,n] 之間的隨機數(shù)為索引,然后找到索引對應(yīng)的節(jié)點,不就是一個隨機的節(jié)點了嗎?
但題目說了,只能遍歷一次,意味著這種思路不可行。題目還可以再泛化,給一個未知長度的序列,如何在其中隨機地選擇 k 個元素?想要解決這個問題,就需要著名的水塘抽樣算法了。
PS:我認真寫了 100 多篇原創(chuàng),手把手刷 200 道力扣題目,全部發(fā)布在labuladong的算法小抄,持續(xù)更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路后投再入題海就如魚得水了。
算法實現(xiàn)
先解決只抽取一個元素的問題,這個問題的難點在于,隨機選擇是「動態(tài)」的,比如說你現(xiàn)在你有 5 個元素,你已經(jīng)隨機選取了其中的某個元素 a 作為結(jié)果,但是現(xiàn)在再給你一個新元素 b,你應(yīng)該留著 a 還是將 b 作為結(jié)果呢,以什么邏輯選擇 a 和 b 呢,怎么證明你的選擇方法在概率上是公平的呢?
先說結(jié)論,當你遇到第 i 個元素時,應(yīng)該有 1/i 的概率選擇該元素,1 - 1/i 的概率保持原有的選擇。看代碼容易理解這個思路:
/* 返回鏈表中一個隨機節(jié)點的值 */ int getRandom(ListNode head) {Random r = new Random();int i = 0, res = 0;ListNode p = head;// while 循環(huán)遍歷鏈表while (p != null) {// 生成一個 [0, i) 之間的整數(shù)// 這個整數(shù)等于 0 的概率就是 1/iif (r.nextInt(++i) == 0) {res = p.val;}p = p.next;}return res; }對于概率算法,代碼往往都是很淺顯的,但是這種問題的關(guān)鍵在于證明,你的算法為什么是對的?為什么每次以 1/i 的概率更新結(jié)果就可以保證結(jié)果是平均隨機(uniform random)?
證明:假設(shè)總共有 n 個元素,我們要的隨機性無非就是每個元素被選擇的概率都是 1/n 對吧,那么對于第 i 個元素,它被選擇的概率就是:
第 i 個元素被選擇的概率是 1/i,第 i+1 次不被替換的概率是 1 - 1/(i+1),以此類推,相乘就是第 i 個元素最終被選中的概率,就是 1/n。
因此,該算法的邏輯是正確的。
同理,如果要隨機選擇 k 個數(shù),只要在第 i 個元素處以 k/i 的概率選擇該元素,以 1 - k/i 的概率保持原有選擇即可。代碼如下:
/* 返回鏈表中 k 個隨機節(jié)點的值 */ int[] getRandom(ListNode head, int k) {Random r = new Random();int[] res = new int[k];ListNode p = head;// 前 k 個元素先默認選上for (int j = 0; j < k && p != null; j++) {res[j] = p.val;p = p.next;}int i = k;// while 循環(huán)遍歷鏈表while (p != null) {// 生成一個 [0, i) 之間的整數(shù)int j = r.nextInt(++i);// 這個整數(shù)小于 k 的概率就是 k/iif (j < k) {res[j] = p.val;}p = p.next;}return res; }對于數(shù)學證明,和上面區(qū)別不大:
因為雖然每次更新選擇的概率增大了 k 倍,但是選到具體第 i 個元素的概率還是要乘 1/k,也就回到了上一個推導。
PS:我認真寫了 100 多篇原創(chuàng),手把手刷 200 道力扣題目,全部發(fā)布在labuladong的算法小抄,持續(xù)更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路后投再入題海就如魚得水了。
拓展延伸
以上的抽樣算法時間復雜度是 O(n),但不是最優(yōu)的方法,更優(yōu)化的算法基于幾何分布(geometric distribution),時間復雜度為 O(k + klog(n/k))。由于涉及的數(shù)學知識比較多,這里就不列出了,有興趣的讀者可以自行搜索一下。
還有一種思路是基于「Fisher–Yates 洗牌算法」的。隨機抽取 k 個元素,等價于對所有元素洗牌,然后選取前 k 個。只不過,洗牌算法需要對元素的隨機訪問,所以只能對數(shù)組這類支持隨機存儲的數(shù)據(jù)結(jié)構(gòu)有效。
另外有一種思路也比較有啟發(fā)意義:給每一個元素關(guān)聯(lián)一個隨機數(shù),然后把每個元素插入一個容量為 k 的二叉堆(優(yōu)先級隊列)按照配對的隨機數(shù)進行排序,最后剩下的 k 個元素也是隨機的。
這個方案看起來似乎有點多此一舉,因為插入二叉堆需要 O(logk) 的時間復雜度,所以整個抽樣算法就需要 O(nlogk) 的復雜度,還不如我們最開始的算法。但是,這種思路可以指導我們解決加權(quán)隨機抽樣算法,權(quán)重越高,被隨機選中的概率相應(yīng)增大,這種情況在現(xiàn)實生活中是很常見的,比如你不往游戲里充錢,就永遠抽不到皮膚。
最后,我想說隨機算法雖然不多,但其實很有技巧的,讀者不妨思考兩個常見且看起來很簡單的問題:
1、如何對帶有權(quán)重的樣本進行加權(quán)隨機抽取?比如給你一個數(shù)組 w,每個元素 w[i] 代表權(quán)重,請你寫一個算法,按照權(quán)重隨機抽取索引。比如 w = [1,99],算法抽到索引 0 的概率是 1%,抽到索引 1 的概率是 99%。
2、實現(xiàn)一個生成器類,構(gòu)造函數(shù)傳入一個很長的數(shù)組,請你實現(xiàn) randomGet 方法,每次調(diào)用隨機返回數(shù)組中的一個元素,多次調(diào)用不能重復返回相同索引的元素。要求不能對該數(shù)組進行任何形式的修改,且操作的時間復雜度是 O(1)。
_____________
我的 在線電子書 有 100 篇原創(chuàng)文章,手把手帶刷 200 道力扣題目,建議收藏!對應(yīng)的 GitHub 算法倉庫 已經(jīng)獲得了 70k star,歡迎標星!
總結(jié)
以上是生活随笔為你收集整理的labuladong的算法小抄pdf_随机算法:水塘抽样算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何查看python安装位置图_怎么查看
- 下一篇: 阿里工作流引擎_免费开源,一款快速开发模