小林手撕?LRU?算法!
時間:2021-08-19 16:26:19
手機(jī)看文章
掃描二維碼
隨時隨地手機(jī)看文章
[導(dǎo)讀]大家好,我是小林。前幾天,我寫一篇感受計算機(jī)基礎(chǔ)之美的文章:堅持一年了里面介紹了個心跳服務(wù)的宕機(jī)判斷算法,當(dāng)時只是理論分析了下使用LRU算法來實現(xiàn),沒有手撕代碼。今天,就帶大家手撕LRU算法,先讓大家回顧下案例,然后后面就進(jìn)行代碼講解。宕機(jī)判斷算法的設(shè)計心跳服務(wù)主要做兩件事情:發(fā)...
大家好,我是小林。前幾天,我寫一篇感受計算機(jī)基礎(chǔ)之美的文章:堅持一年了里面介紹了個心跳服務(wù)的宕機(jī)判斷算法,當(dāng)時只是理論分析了下使用 LRU 算法來實現(xiàn),沒有手撕代碼。今天,就帶大家手撕 LRU 算法,先讓大家回顧下案例,然后后面就進(jìn)行代碼講解。 由于采用的數(shù)據(jù)結(jié)構(gòu)是雙向鏈表,所以隊尾插入和隊頭刪除操作的時間復(fù)雜度是 O(1)。如果有新的心跳包,則將其插入到雙向鏈表的尾部,那么最老的心跳包就是在雙向鏈表的頭部,這樣在尋找宕機(jī)的主機(jī)時,只要看雙向鏈表頭部最老的心跳包,距現(xiàn)在是否超過 5 秒,如果超過 5秒 則認(rèn)為該主機(jī)宕機(jī),然后將其從雙向鏈表中刪除。細(xì)心的同學(xué)肯定發(fā)現(xiàn)了個問題,就是如果一個主機(jī)的心跳包已經(jīng)在隊列中,那么下次該主機(jī)的心跳包要怎么處理呢?為了維持隊列里的心跳包是主機(jī)最新上報的,所以要先找到該主機(jī)舊的心跳包,然后將其刪除,再把新的心跳包插入到雙向鏈表的隊尾。問題來了,在隊列找到該主機(jī)舊的心跳包,由于數(shù)據(jù)結(jié)構(gòu)是雙向鏈表,所以這個查詢過程的時間復(fù)雜度時 O(N),也就是說隨著隊列里的元素越多,會越影響程序的性能,這一點我們必須優(yōu)化。查詢效率最好的數(shù)據(jù)結(jié)構(gòu)就是「哈希表」了,時間復(fù)雜度只有 O(1),因此我們可以加入這個數(shù)據(jù)結(jié)構(gòu)來優(yōu)化。哈希表的 Key 是主機(jī)的 IP 地址,Value 包含主機(jī)在雙向鏈表里的節(jié)點,這樣我們就可以通過哈希表輕松找到該主機(jī)在雙向鏈表中的位置。 這樣,每當(dāng)收到心跳包時,先判斷其在不在哈希表里。
要將主機(jī)從哈希表刪除,首先我們要知道主機(jī)的 IP,因為這是哈希表的 Key。雙向鏈表存儲的內(nèi)容必須包含主機(jī)的 IP 信息,那為了更快查詢到主機(jī)的 IP,雙向鏈表存儲的內(nèi)容可以是一個鍵值對(Key-Value),其 Key 就是主機(jī)的 IP,Value 就是主機(jī)的信息。 這樣,在發(fā)現(xiàn)雙向鏈表中頭部的節(jié)點超時了,由于節(jié)點的內(nèi)容是鍵值對,于是就能快速地從該節(jié)點獲取主機(jī)的 IP ,知道了主機(jī)的 IP 信息,就能把哈希表中該主機(jī)信息刪除。至此,就設(shè)計出了一個高性能的宕機(jī)判斷算法,主要用了數(shù)據(jù)結(jié)構(gòu):哈希表 雙向鏈表,通過這個組合,查詢 刪除 插入操作的時間復(fù)雜度都是 O(1),以空間換時間的思想,這就是數(shù)據(jù)結(jié)構(gòu)與算法之美!熟悉算法的同學(xué)應(yīng)該感受出來了,上面這個算法就是類 LRU 算法,用于淘汰最近最久使用的元素的場景,該算法應(yīng)用范圍很廣的,操作系統(tǒng)、Redis、MySQL 都有使用該算法。
宕機(jī)判斷算法的設(shè)計
心跳服務(wù)主要做兩件事情:- 發(fā)現(xiàn)宕機(jī)的主機(jī);
- 發(fā)現(xiàn)上線的主機(jī)。
- 如果不存在哈希表里,說明是新主機(jī)上線,先將其插入到雙向鏈表的頭部,然后將該主機(jī)的 IP 作為 Key,主機(jī)在雙向鏈表的節(jié)點作為 Value 插入到哈希表。
- 如果存在哈希表里,說明主機(jī)已經(jīng)上線過,先通過查詢哈希表,找到該主機(jī)在雙向鏈表里舊的心跳包的節(jié)點,然后就可以通過該節(jié)點將其從雙向鏈表中刪除,最后將新的心跳包插入到雙向鏈表的隊尾,同時更新哈希表。
要將主機(jī)從哈希表刪除,首先我們要知道主機(jī)的 IP,因為這是哈希表的 Key。雙向鏈表存儲的內(nèi)容必須包含主機(jī)的 IP 信息,那為了更快查詢到主機(jī)的 IP,雙向鏈表存儲的內(nèi)容可以是一個鍵值對(Key-Value),其 Key 就是主機(jī)的 IP,Value 就是主機(jī)的信息。
手撕 LRU 算法
在很多大廠面試的時候,經(jīng)常會考察 LRU 算法,甚至?xí)笫謱懗鰜?,之前就有朋友在面試鵝廠的時候,當(dāng)初就要手寫 LRU 算法。今天,就帶大家用 C 語言手撕 LRU 算法,我們就采用上面討論的「哈希表 雙向鏈表」這兩個數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)該算法。為了要實現(xiàn) LRU 算法, 鏈表的隊頭要保持是最近訪問或者新加入的數(shù)據(jù),鏈表的隊尾要保持是最久未被訪問的,這樣我們在淘汰最久未訪問的時候會很簡單,然后哈希表用于快速查找節(jié)點。雙向鏈表,存放的內(nèi)容是鍵值對。typedef?std::pair<int?key,?std::string?value>?Pair;
typedef?std::list?List;
哈希表,存放的是鏈表節(jié)點。typedef?std::map<int?key,?typename?List::iterator>?Map;
知道了數(shù)據(jù)結(jié)構(gòu)后,然后實現(xiàn)兩個函數(shù),分別是 put
用于加入數(shù)據(jù),get
用戶獲取數(shù)據(jù),我這里定義了個 LRUCache
模板類,如下:接下來,看看存放數(shù)據(jù)的 put
方法實現(xiàn)的方式,如下:說一下 put 方法的實現(xiàn)思路。首先,通過哈希表查找是否存在該 Key:- 如果存在則表示有老數(shù)據(jù),那么就需要將老數(shù)據(jù)先從鏈表和哈希表里刪除,然后再將新的數(shù)據(jù)重新加入到鏈表的隊頭,同時該鏈表節(jié)點存放到哈希表里,這樣鏈表里就維護(hù)了該 key 數(shù)據(jù)是最熱的。
- 如果哈希表不存在該 Key,則認(rèn)為是新的數(shù)據(jù),直接將其加入到鏈表的隊頭,并把該鏈表節(jié)點更新到哈希表里。
get
方法的實現(xiàn)方式,如下:首先先在哈希表中查找是否存在該 key:- 如果不存在,則返回 false;
- 如果存在,則鏈表要將數(shù)據(jù)刪除,然后再數(shù)據(jù)加入到鏈表隊頭,目的是為了維持鏈表隊頭是最近訪問的數(shù)據(jù)。
3
的 LRUCache 對象,然后使用 put 函數(shù)加入 3 組 key-value,這時鏈表的順序是 key:3(隊頭) -> ?key:2 -> key:1(隊尾)。然后通過 get 訪問 key:1
的元素,這時鏈表的順序變?yōu)?key:1(隊頭) -> ?key:3 -> key:2(隊尾)。接著,put 加入 key:4
元素,由于鏈表的大小超過了定義的 LRUCache 的容量,于是就會移除隊尾的元素,也就是 key:2
。最后看到,就無法訪問 key:2 元素的了,運行結(jié)果如下。好了,LRU 算法手撕就到了啦。我是小林,今天你比昨天更博學(xué)了嗎?