高级数据结构与算法 | LFU缓存机制(Least Frequently Used)
文章目錄
- LFUCache
- 結構設計
- LFUCache的實現
在之前我寫過一篇LRU的博客,如果不了解的建議先看看這篇
高級數據結構與算法 | LRU緩存機制(Least Recently Used)
LFUCache
LFU(Least Frequently Used),即最不經常使用頁置換算法。
如果一個數據在最近一段時間很少被訪問到,那么可以認為在將來它被訪問的可能性也很小。因此,當空間滿時,最小頻率訪問的數據最先被淘汰,這就是LFU的核心思想。
如果我們想要設計一個LFU緩存算法,就需要滿足以下功能
- 初始化時設置緩存大小,當緩存已滿后插入元素時需要淘汰現有元素來置換新元素
- get(key):如果緩存中存在key,則獲取value,并更新其使用次數
- put(key, value) :如果key已經存在,則更新key所對應的value。如果不存在則插入鍵值對,但是插入時如果緩存已滿,則需要將最不常用的鍵值對淘汰,置換新數據。如果存在多個使用頻率相同的鍵值對,應當選取最久沒有使用的。
- 保證get和put的操作時間復雜度都為O(1)
根據以上條件,我們在上面給出了一個算法的大致框架,下面就結合框架以及條件,來設計我們具體的一個結構
結構設計
由于LFU需要根據使用次數來進行緩存淘汰的判斷,因此我們每一項中不僅需要存儲key,value,還需要存儲使用次數freq
每一項的節點設計如下
class LFUNode { public:LFUNode(int key, int value, int freq): _key(key), _value(value), _freq(freq){}int _key; int _value;int _freq; //訪問次數 };在上面列的幾個條件中,其中最難處理的地方其實就是O(1)的get和put,因為沒有任何一個獨立的數據結構能夠滿足這個條件,因此我們需要組合使用多個結構。
如果要保證put中的插入和刪除操作達到O(1),那么我們只能選擇使用鏈表來記錄我們的緩存信息,但是這次與之前的LRU并不相同,我們在保證使用次數最少的同時,還需要保證淘汰的時同次數中最久沒有使用的,這就意味這我們需要根據不同的使用次數,來維護多個鏈表,并且每個鏈表中按照使用時間進行排序。
上面的設計依舊存在著一些問題,因為我們在對一個元素進行操作后,它的使用次數就會發生變化,那么我們就需要將其從原先的鏈表中移動到下一個鏈表,那么我們如何能夠保證O(1)的查找到它,并對他進行操作呢?同時,我們如何能夠快速的在多個鏈表中,查找到它所在的那個鏈表,以及它即將要去的鏈表呢?因此,我們還需要結合其他的數據結構來完成這個問題
要保證O(1)的查找鏈表,這時候就需要用到哈希結構了,我們可以將使用次數freq與每個保存該freq節點的鏈表建立起映射,保證O(1)的獲取鏈表。
如果要保證O(1)的從指定鏈表中找到元素,并對其進行操作,我們可以將key與鏈表中對應元素的迭代器建立起映射,當我們通過key找到迭代器的時候,就可以利用迭代器對鏈表進行操作。
因此,最終LFUCache的結構設計如下
class LFUCache { public:LFUCache(int capacity) : _capacity(capacity), _minFreq(0){}int get(int key) {}void put(int key, int value) {}private:unordered_map<int, list<LFUNode>::iterator> _keyTable; //建立key與節點迭代器的映射unordered_map<int, list<LFUNode>> _freqTable; //建立freq與該值鏈表的映射int _minFreq; //保存當前最小的freq,便于淘汰操作int _capacity; //緩存最大容量 };LFUCache的實現
接下來就該實現get和put功能了,我先列出這兩個功能實現的核心思路
get
- 根據keyTable查找到節點的迭代器,不存在則直接返回
- 更新使用次數,通過迭代器將節點從原鏈表中刪除,并通過freqTable獲取下一個鏈表,將其頭插進鏈表中,保證最后使用的在最前面,使得鏈表按照使用時間倒序排序
- 將新鏈表中該節點的迭代器更新到keyTable中
put
- 根據keyTable查找到節點的迭代器,存在則說明是更新,不存在則說明是淘汰
- 如果是更新,那么操作的內容和上面的get大體相同
- 如果是插入,需要判斷當前緩存是否已滿,是否需要淘汰,如果未滿則直接插入freq=1的鏈表中,并將迭代器插入至keyTable中
- 如果緩存已滿,則需要根據minFreq在freqTable中找到對應的最小freq鏈表,并刪除隊尾,即最久沒有使用的元素,同時在keyTable中淘汰該元素
這里我就將每一步的思路寫在下面代碼的注釋中
完整的實現代碼如下
class LFUNode { public:LFUNode(int key, int value, int freq): _key(key), _value(value), _freq(freq){}int _key; int _value;int _freq; //訪問次數 };class LFUCache { public:LFUCache(int capacity) : _capacity(capacity), _minFreq(0){}int get(int key) {if(_capacity == 0){return -1;}auto it = _keyTable.find(key); //查找到對應的節點if(it != _keyTable.end()){list<LFUNode>::iterator node = it->second; //記錄節點的value以及freqint value = node->_value, freq = node->_freq;_freqTable[freq].erase(node); //從freq表中的對應鏈表刪除當前節點if(_freqTable[freq].empty() && _minFreq == freq) //如果刪除后鏈表為空,則判斷是否需要更新最小freq{_minFreq++;}_freqTable[freq + 1].push_front(LFUNode(key, value, freq + 1)); //將更新后的節點頭插入下一個freq的鏈表中,保證按照插入時間排序it->second = _freqTable[freq + 1].begin(); //更新迭代器return value;}//查找不到直接返回-1else{return -1;}}void put(int key, int value) {if(_capacity == 0){return;}auto it = _keyTable.find(key); //查找key是否存在,判斷是更新還是插入if(it != _keyTable.end()) //如果存在,則是更新{list<LFUNode>::iterator node = it->second;int freq = node->_freq;_freqTable[freq].erase(node);if(_freqTable[freq].empty() && _minFreq == freq){_minFreq++;}_freqTable[freq + 1].push_front(LFUNode(key, value, freq + 1));it->second = _freqTable[freq + 1].begin();}else //不存在則是插入{//如果緩存滿了,則要先刪除后才能插入if(_keyTable.size() == _capacity){//查詢freq表,獲取freq最少且最久沒有使用的節點,即freq鏈表的尾部節點LFUNode node = _freqTable[_minFreq].back();_keyTable.erase(node._key); //刪除key表中的節點_freqTable[node._freq].pop_back(); //刪除freq表中的節點}_freqTable[1].push_front(LFUNode(key, value, 1)); //在freq表中插入新節點_keyTable.insert(make_pair(key, _freqTable[1].begin()));//在key表中插入新節點_minFreq = 1; //插入新元素,它為最小的}} private:unordered_map<int, list<LFUNode>::iterator> _keyTable; //建立key與節點迭代器的映射unordered_map<int, list<LFUNode>> _freqTable; //建立freq與該值鏈表的映射int _minFreq; //保存當前最小的freq,便于淘汰int _capacity; //緩存最大容量 };總結
以上是生活随笔為你收集整理的高级数据结构与算法 | LFU缓存机制(Least Frequently Used)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis 过期键删除策略、内存淘汰机制
- 下一篇: Linux 内存管理 | 物理内存管理: