緩存淘汰算法 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章