快取淘汰演算法 LRU 和 LFU【轉】
轉自:https://www.jianshu.com/p/1f8e36285539
快取是一個計算機思維,對於重複的計算,快取其結果,下次再算這個任務的時候,不去真正的計算,而是直接返回結果,能加快處理速度。當然有些會隨時間改變的東西,快取會失效,得重新計算。
比如快取空間只有2個,要快取的資料有很多,1,2,3,4,5,那麼當快取空間滿了,需要淘汰一個快取出去,其中淘汰演算法有 LRU,LFU,FIFO,SC二次機會,老化演算法,時鐘工作集演算法等等。
演算法流程
LRU,最近最少使用,把資料加入一個連結串列中,按訪問時間排序,發生淘汰的時候,把訪問時間最舊的淘汰掉。
比如有資料 1,2,1,3,2
此時快取中已有(1,2)
當3加入的時候,得把後面的2淘汰,變成(3,1)
LFU,最近不經常使用,把資料加入到連結串列中,按頻次排序,一個數據被訪問過,把它的頻次+1,發生淘汰的時候,把頻次低的淘汰掉。
比如有資料 1,1,1,2,2,3
快取中有(1(3次),2(2次))
當3加入的時候,得把後面的2淘汰,變成(1(3次),3(1次))
區別:LRU 是得把 1 淘汰。
顯然
LRU對於迴圈出現的資料,快取命中不高
比如,這樣的資料,1,1,1,2,2,2,3,4,1,1,1,2,2,2.....
當走到3,4的時候,1,2會被淘汰掉,但是後面還有很多1,2
LFU對於交替出現的資料,快取命中不高
比如,1,1,1,2,2,3,4,3,4,3,4,3,4,3,4,3,4......
由於前面被(1(3次),2(2次))
3加入把2淘汰,4加入把3淘汰,3加入把4淘汰,然而3,4才是最需要快取的,1去到了3次,誰也淘汰不了它了。
實現
leetcode上有兩個題目
LRU:https://leetcode.com/problems/lru-cache/description/
LFU:https://leetcode.com/problems/lfu-cache/description/
要求是快取的加入put(),快取讀取get(),都要在O(1)內實現。
LRU的一個實現方法:
用一個雙向連結串列記錄訪問時間,因為連結串列插入刪除高效,時間新的在前面,舊的在後面。
用一個雜湊表記錄快取(key, value),雜湊查詢近似O(1),發生雜湊衝突時最壞O(n),同時雜湊表中得記錄 (key, (value, key_ptr)),key_ptr 是key在連結串列中的地址,為了能在O(1)時間內找到該節點,並把節點提升到表頭。
連結串列中的key,能快速找到hash中的value,並刪除。
LFU的一個實現方法:
用一個主雙向連結串列記錄(訪問次數,從連結串列頭),從連結串列中按時間順序記錄著(key)
用一個雜湊表記錄(key,(value, 主連結串列ptr,從連結串列ptr))ptr表示該key在連結串列中的地址
然後,get,put都在雜湊表中操作,近似O(1),雜湊表中有個節點在連結串列中的地址,能O(1)找到,並把節點提搞訪問頻次,連結串列插入刪除也都是O(1)。
-------------------- 最後貼個AC的程式碼:--------------------
程式碼效能:1000000次加入,讀取用時
LRU: 480ms
LFU: 510ms
NSCache: 2000ms
YYCache: 1400ms
LRU:
#include <list>
#include <unordered_map>
using namespace std;
class LRUCache {
public:
LRUCache(int capacity);
~LRUCache();
int get(int key); // 獲取快取,hash查詢的複雜度
void put(int key, int value); // 加入快取,相同的key會覆蓋,hash插入的複雜度
private:
int max_capacity;
list<pair<int, int>> m_list; // 雙向連結串列,pair<key, value>
unordered_map<int, list<pair<int, int>>::iterator> u_map; // 雜湊map, vector + list 實現,<key, list::iter>
};
LRUCache::LRUCache(int capacity) {
max_capacity = capacity;
}
LRUCache::~LRUCache() {
max_capacity = 0;
u_map.clear();
m_list.clear();
}
int LRUCache::get(int key) {
auto it = u_map.find(key); // C++11 自動型別推斷
if (it != u_map.end()) {
// splice() 合併 將 m_list 的 iter 移動到 m_list.begin() 中
m_list.splice(m_list.begin(), m_list, it->second);
return it->second->second; // return value
}
return -1;
}
void LRUCache::put(int key, int value) {
auto it = u_map.find(key);
if (it != u_map.end()) {
// 更新 key 的 value,並把 key 提前
it->second->second = value;
m_list.splice(m_list.begin(), m_list, it->second);
} else {
// 先判斷是否滿,滿了要刪除
if (m_list.size() >= max_capacity) {
int del_key = m_list.back().first;
u_map.erase(del_key);
m_list.pop_back();
}
// 插入到 u_map, list 中
m_list.emplace_front(key, value); // emplace_front 與 puch_front, emplace_front 不拷貝節點,不移動元素,高效
u_map[key] = m_list.begin();
}
}
LFU:
#include <list>
#include <unordered_map>
using namespace std;
// map value 結構
typedef struct LFUMapValue {
int value;
list<pair<int, list<int> > >::iterator main_it;
list<int>::iterator sub_it;
} LFUMapValue;
class LFUCache {
public:
LFUCache(int capacity);
~LFUCache();
int get(int key);
void put(int key, int value);
void right_move(LFUMapValue *value); // 把一個節點的key向右提高訪問次數
private:
int max_cap;
int cur_cap;
// 儲存 pair<count, subList<key> > 結構,count 訪問次數,count 小到大,key 時間由新到舊
list<pair<int, list<int> > > m_list;
unordered_map<int, LFUMapValue> u_map; // 儲存 <key, LFUMapValue> 結構
unordered_map<int, LFUMapValue>::iterator map_it;
};
LFUCache::LFUCache(int capacity) {
cur_cap = 0;
max_cap = capacity;
m_list.emplace_front(pair<int, list<int> >(1, list<int>())); // 插入 count == 1 的節點
}
LFUCache::~LFUCache() {
m_list.clear();
u_map.clear();
}
void LFUCache::right_move(LFUMapValue *value) {
auto pre = value->main_it;
auto pre_sub_it = value->sub_it;
auto next = pre;
next++;
if (next != m_list.end()) {
if (pre->first + 1 != next->first) { // 訪問次數+1,判斷是否相等
if (pre->second.size() == 1) {
pre->first++; // 這個 count 的 list 只有1個key,原地+1,不建立新節點
} else {
// next 前插入一個節點
auto it = m_list.emplace(next, pair<int, list<int> >(pre->first + 1, list<int>()));
it->second.splice(it->second.begin()