精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

一文讀懂緩存淘汰算法:LFU 算法

算法與數據結構 ? 來源:labuladong ? 作者:labuladong ? 2020-08-25 17:37 ? 次閱讀

作者:labuladong

上篇文章算法題就像搭樂高:手把手帶你拆解 LRU 算法寫了 LRU 緩存淘汰算法的實現方法,本文來寫另一個著名的緩存淘汰算法:LFU 算法。

從實現難度上來說,LFU 算法的難度大于 LRU 算法,因為 LRU 算法相當于把數據按照時間排序,這個需求借助鏈表很自然就能實現,你一直從鏈表頭部加入元素的話,越靠近頭部的元素就是新的數據,越靠近尾部的元素就是舊的數據,我們進行緩存淘汰的時候只要簡單地將尾部的元素淘汰掉就行了。

而 LFU 算法相當于是淘汰訪問頻次最低的數據,如果訪問頻次最低的數據有多條,需要淘汰最舊的數據。把數據按照訪問頻次進行排序,而且頻次還會不斷變化,這可不容易實現。

所以說 LFU 算法要復雜很多,labuladong 進字節跳動的時候就被面試官問到了 LFU 算法。

話說回來,這種著名的算法的套路都是固定的,關鍵是由于邏輯較復雜,不容易寫出漂亮且沒有 bug 的代碼。

那么本文 labuladong 就帶你拆解 LFU 算法,自頂向下,逐步求精。

一、算法描述

要求你寫一個類,接受一個capacity參數,實現get和put方法:

classLFUCache{
//構造容量為capacity的緩存
publicLFUCache(intcapacity){}
//在緩存中查詢key
publicintget(intkey){}
//將key和val存入緩存
publicvoidput(intkey,intval){}
}

get(key)方法會去緩存中查詢鍵key,如果key存在,則返回key對應的val,否則返回 -1。

put(key, value)方法插入或修改緩存。如果key已存在,則將它對應的值改為val;如果key不存在,則插入鍵值對(key, val)。

當緩存達到容量capacity時,則應該在插入新的鍵值對之前,刪除使用頻次(后文用freq表示)最低的鍵值對。如果freq最低的鍵值對有多個,則刪除其中最舊的那個。

//構造一個容量為2的LFU緩存
LFUCachecache=newLFUCache(2);

//插入兩對(key,val),對應的freq為1
cache.put(1,10);
cache.put(2,20);

//查詢key為1對應的val
//返回10,同時鍵1對應的freq變為2
cache.get(1);

//容量已滿,淘汰freq最小的鍵2
//插入鍵值對(3,30),對應的freq為1
cache.put(3,30);

//鍵2已經被淘汰刪除,返回-1
cache.get(2);

二、思路分析

一定先從最簡單的開始,根據 LFU 算法的邏輯,我們先列舉出算法執行過程中的幾個顯而易見的事實:

1、調用get(key)方法時,要返回該key對應的val。

2、只要用get或者put方法訪問一次某個key,該key的freq就要加一。

3、如果在容量滿了的時候進行插入,則需要將freq最小的key刪除,如果最小的freq對應多個key,則刪除其中最舊的那一個。

好的,我們希望能夠在 O(1) 的時間內解決這些需求,可以使用基本數據結構來逐個擊破:

1、使用一個HashMap存儲key到val的映射,就可以快速計算get(key)。

HashMapkeyToVal;

2、使用一個HashMap存儲key到freq的映射,就可以快速操作key對應的freq。

HashMapkeyToFreq;

3、這個需求應該是 LFU 算法的核心,所以我們分開說。

3.1首先,肯定是需要freq到key的映射,用來找到freq最小的key。

3.2、將freq最小的key刪除,那你就得快速得到當前所有key最小的freq是多少。想要時間復雜度 O(1) 的話,肯定不能遍歷一遍去找,那就用一個變量minFreq來記錄當前最小的freq吧。

3.3、可能有多個key擁有相同的freq,所以freq對key是一對多的關系,即一個freq對應一個key的列表。

3.4、希望freq對應的key的列表是存在時序的,便于快速查找并刪除最舊的key。

3.5、希望能夠快速刪除key列表中的任何一個key,因為如果頻次為freq的某個key被訪問,那么它的頻次就會變成freq+1,就應該從freq對應的key列表中刪除,加到freq+1對應的key的列表中。

HashMap>freqToKeys;
intminFreq=0;

介紹一下這個LinkedHashSet,它滿足我們 3.3,3.4,3.5 這幾個要求。你會發現普通的鏈表LinkedList能夠滿足 3.3,3.4 這兩個要求,但是由于普通鏈表不能快速訪問鏈表中的某一個節點,所以無法滿足 3.5 的要求。

LinkedHashSet顧名思義,是鏈表和哈希集合的結合體。鏈表不能快速訪問鏈表節點,但是插入元素具有時序;哈希集合中的元素無序,但是可以對元素進行快速的訪問和刪除。

那么,它倆結合起來就兼具了哈希集合和鏈表的特性,既可以在 O(1) 時間內訪問或刪除其中的元素,又可以保持插入的時序,高效實現 3.5 這個需求。

綜上,我們可以寫出 LFU 算法的基本數據結構:

classLFUCache{
//key到val的映射,我們后文稱為KV表
HashMapkeyToVal;
//key到freq的映射,我們后文稱為KF表
HashMapkeyToFreq;
//freq到key列表的映射,我們后文稱為FK表
HashMap>freqToKeys;
//記錄最小的頻次
intminFreq;
//記錄LFU緩存的最大容量
intcap;

publicLFUCache(intcapacity){
keyToVal=newHashMap<>();
keyToFreq=newHashMap<>();
freqToKeys=newHashMap<>();
this.cap=capacity;
this.minFreq=0;
}

publicintget(intkey){}

publicvoidput(intkey,intval){}

}

三、代碼框架

LFU 的邏輯不難理解,但是寫代碼實現并不容易,因為你看我們要維護KV表,KF表,FK表三個映射,特別容易出錯。對于這種情況,labuladong 教你三個技巧:

1、不要企圖上來就實現算法的所有細節,而應該自頂向下,逐步求精,先寫清楚主函數的邏輯框架,然后再一步步實現細節。

2、搞清楚映射關系,如果我們更新了某個key對應的freq,那么就要同步修改KF表和FK表,這樣才不會出問題。

3、畫圖,畫圖,畫圖,重要的話說三遍,把邏輯比較復雜的部分用流程圖畫出來,然后根據圖來寫代碼,可以極大減少出錯的概率。

下面我們先來實現get(key)方法,邏輯很簡單,返回key對應的val,然后增加key對應的freq:

publicintget(intkey){
if(!keyToVal.containsKey(key)){
return-1;
}
//增加key對應的freq
increaseFreq(key);
returnkeyToVal.get(key);
}

增加key對應的freq是 LFU 算法的核心,所以我們干脆直接抽象成一個函數increaseFreq,這樣get方法看起來就簡潔清晰了對吧。

下面來實現put(key, val)方法,邏輯略微復雜,我們直接畫個圖來看:

一文讀懂緩存淘汰算法:LFU 算法

這圖就是隨手畫的,不是什么正規的程序流程圖,但是算法邏輯一目了然,看圖可以直接寫出put方法的邏輯:

publicvoidput(intkey,intval){
if(this.cap<=?0)?return;

????/*?若?key?已存在,修改對應的?val?即可?*/
????if?(keyToVal.containsKey(key))?{
????????keyToVal.put(key,?val);
????????//?key?對應的?freq?加一
????????increaseFreq(key);
????????return;
????}

????/*?key?不存在,需要插入?*/
????/*?容量已滿的話需要淘汰一個?freq?最小的?key?*/
????if?(this.cap?<=?keyToVal.size())?{
????????removeMinFreqKey();
????}

????/*?插入?key?和?val,對應的?freq?為?1?*/
????//?插入?KV?表
????keyToVal.put(key,?val);
????//?插入?KF?表
????keyToFreq.put(key,?1);
????//?插入?FK?表
????freqToKeys.putIfAbsent(1,?new?LinkedHashSet<>());
freqToKeys.get(1).add(key);
//插入新key后最小的freq肯定是1
this.minFreq=1;
}

increaseFreq和removeMinFreqKey方法是 LFU 算法的核心,我們下面來看看怎么借助KV表,KF表,FK表這三個映射巧妙完成這兩個函數。

四、LFU 核心邏輯

首先來實現removeMinFreqKey函數:

privatevoidremoveMinFreqKey(){
//freq最小的key列表
LinkedHashSetkeyList=freqToKeys.get(this.minFreq);
//其中最先被插入的那個key就是該被淘汰的key
intdeletedKey=keyList.iterator().next();
/*更新FK表*/
keyList.remove(deletedKey);
if(keyList.isEmpty()){
freqToKeys.remove(this.minFreq);
//問:這里需要更新 minFreq 的值嗎?
}
/*更新KV表*/
keyToVal.remove(deletedKey);
/*更新KF表*/
keyToFreq.remove(deletedKey);
}

刪除某個鍵key肯定是要同時修改三個映射表的,借助minFreq參數可以從FK表中找到freq最小的keyList,根據時序,其中第一個元素就是要被淘汰的deletedKey,操作三個映射表刪除這個key即可。

但是有個細節問題,如果keyList中只有一個元素,那么刪除之后minFreq對應的key列表就為空了,也就是minFreq變量需要被更新。如何計算當前的minFreq是多少呢?

實際上沒辦法快速計算minFreq,只能線性遍歷FK表或者KF表來計算,這樣肯定不能保證 O(1) 的時間復雜度。

但是,其實這里沒必要更新minFreq變量,因為你想想removeMinFreqKey這個函數是在什么時候調用?在put方法中插入新key時可能調用。而你回頭看put的代碼,插入新key時一定會把minFreq更新成 1,所以說即便這里minFreq變了,我們也不需要管它。

下面來實現increaseFreq函數:

privatevoidincreaseFreq(intkey){
intfreq=keyToFreq.get(key);
/*更新KF表*/
keyToFreq.put(key,freq+1);
/*更新FK表*/
//將key從freq對應的列表中刪除
freqToKeys.get(freq).remove(key);
//將key加入freq+1對應的列表中
freqToKeys.putIfAbsent(freq+1,newLinkedHashSet<>());
freqToKeys.get(freq+1).add(key);
//如果freq對應的列表空了,移除這個freq
if(freqToKeys.get(freq).isEmpty()){
freqToKeys.remove(freq);
//如果這個freq恰好是minFreq,更新minFreq
if(freq==this.minFreq){
this.minFreq++;
}
}
}

更新某個key的freq肯定會涉及FK表和KF表,所以我們分別更新這兩個表就行了。

和之前類似,當FK表中freq對應的列表被刪空后,需要刪除FK表中freq這個映射。如果這個freq恰好是minFreq,說明minFreq變量需要更新。

能不能快速找到當前的minFreq呢?這里是可以的,因為我們剛才把key的freq加了 1 嘛,所以minFreq也加 1 就行了。

至此,經過層層拆解,LFU 算法就完成了。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 算法
    +關注

    關注

    23

    文章

    4601

    瀏覽量

    92677
  • 數據結構
    +關注

    關注

    3

    文章

    573

    瀏覽量

    40095

原文標題:算法題就像搭樂高:手把手帶你拆解 LFU 算法

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數據結構】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    《CDN 之我見》系列二:原理篇(緩存、安全)

    Hash 運算都得到同個余數),則性能與單鏈表無異,查找時間復雜度是 O(n)。如果磁盤空間不夠了怎么辦?使用基于訪問熱度的內容淘汰算法,例如 FIFO、LRU、LFU、SLRU、
    發表于 06-12 16:59

    深度學習RCNN算法

    目標檢測算法圖解:看懂RCNN系列算法
    發表于 08-29 09:50

    讀懂接口模塊的組合應用有哪些?

    讀懂接口模塊的組合應用有哪些?
    發表于 05-17 07:15

    讀懂什么是NEC協議

    讀懂什么是NEC協議?
    發表于 10-15 09:22

    星上交換系統輸入緩存調度算法

    為改善星上交換系統的性能,該文提出了種新的輸入緩存調度算法。該算法基于Crossbar 交換結構,采用了串行調度思想,在兼顧每個端口公平性的基礎上調整了輸出端口的仲裁策
    發表于 11-17 13:52 ?10次下載

    HSDPA系統中種感知用戶終端緩存狀態的QoE保障調度算法

    針對HSDPA系統中現有調度算法無法滿足實時業務QoE的缺點,提出種保障實時業務QoE的調度算法。該算法根據用戶反饋的信道質量信息和在基站獲取到的用戶終端
    發表于 01-08 15:24 ?0次下載

    基于BCH算法的高速緩存糾檢錯方案研究

    基于BCH算法的高速緩存糾檢錯方案研究
    發表于 01-07 20:32 ?0次下載

    讀懂數據結構中的算法

    在進步學習數據結構與算法前,我們應該先掌握算法分析的般方法。算法分析主要包括對算法的時空復雜
    的頭像 發表于 11-15 15:19 ?3429次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>數據結構中的<b class='flag-5'>算法</b>

    基于DASH的混合控制碼率算法

    針對平滑流(SF)算法在帶寬預測時存在的毛刺現象以及僅依靠帶寬預測而沒有緩存區控制所導致的頻繁播放停滯的問題,提出種動態自適應混合控制碼率算法。首先,通過使用標準差來代替原SF
    發表于 11-24 16:54 ?0次下載
    基于DASH的混合控制碼率<b class='flag-5'>算法</b>

    基于密策略屬性基加密系統訪問機制的緩存替換策略

    為提高基于密策略屬性基加密( CP-ABE)系統的數據緩存性能,針對CP-ABE加密的數據,提出種有效的緩存替換算法最小屬性價值(MAV
    發表于 11-25 11:13 ?0次下載

    讀懂幾種常用的安全算法

    摘要算法 ? 對稱加密算法 ? 非對稱加密算法 ? 數字簽名 ? 數字證書 數字摘要 實現 ? 將任意長度的明文通過單向hash函數摘要成固定長度的串。 Hash(明文)--固定長度的摘要 特點
    發表于 05-30 01:59 ?1796次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>幾種常用的安全<b class='flag-5'>算法</b>

    基于哈希算法和近鄰算法緩存數據選擇策略

    針對終端用戶產生大量相同或相似計算請求的情況,可以通過近似匹配在邊緣服務器緩存空間中查找相似數據,選取可復用的計算結果?,F有算法大多未考慮數據分布不均的問題,導致計算量和時間開銷較大,對此
    發表于 04-19 15:11 ?3次下載
    基于哈希<b class='flag-5'>算法</b>和近鄰<b class='flag-5'>算法</b>的<b class='flag-5'>緩存</b>數據選擇策略

    緩存敏感的多屬性不等值連接操作算法

    緩存敏感的多屬性不等值連接操作算法
    發表于 06-25 16:16 ?5次下載

    讀懂經典雙目稠密匹配算法SGM

    最近來看看些雙目稠密匹配的算法。說來慚愧,SGM在航測領域是很重要的算法(當然也是最好的雙目稠密匹配算法),自己卻沒有認真讀過,只是大
    的頭像 發表于 12-15 15:12 ?1445次閱讀

    讀懂,什么是BLE?

    讀懂,什么是BLE?
    的頭像 發表于 11-27 17:11 ?2200次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>,什么是BLE?