LRU算法原理和實現

01 題目介紹

題目描述:

leetcode 146 LRU緩存機制中等難度

運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持以下操作:獲取數據 get 和寫入數據 put 。

獲取數據 get(key) - 如果密鑰 (key) 存在於緩存中,則獲取密鑰的值(總是正數),否則返回 -1。

寫入數據 put(key, value) - 如果密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據之前刪除最近最少使用的數據值,從而爲新的數據值留出空間。

要求: O(1) 時間複雜度完成這兩種操作


02 題目分析

概念

LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那麼將來被訪問的機率也更高”。

重點

1 最近被訪問的數據,其優先級最高;

2 優先級低的數據最先被清除;

時間複雜度

O(1)


03 可行方案

1 鏈表結構

使用鏈表結構保存緩存數據。

每當執行put操作時,遍歷鏈表判斷該數據是否爲新數據:
若爲新數據,則在鏈表頭部新建節點並存放新數據;當鏈表長度超過緩存大小時,將鏈表尾部節點刪除。
若爲舊數據,則說明緩存數據命中,更新該緩存數據,並將命中的鏈表節點移到鏈表頭部。


每當執行get操作時,通過遍歷鏈表進行緩存數據的尋找: 若命中,則根據密鑰(key)返回數據值(value),並將數據所在的鏈表節點置於鏈表頭部; 若未命中,則說明該數據不在緩存中,返回-1。

問題:鏈表在使用的時候,爲了確定是否命中,需要對鏈表結構進行遍歷。時間複雜度爲o(n),n爲鏈表長度。未滿足題目要求。


2 雙向鏈表與哈希表結合

利用雙向鏈表保存緩存數據,利用哈希表解決需要遍歷尋找命中的問題。

雙向鏈表中存放的是緩存數據;哈希表中的value值對應於雙向鏈表中的節點地址。
在這裏插入圖片描述

每當執行put操作時,先判斷插入的的鍵值對中的key是否存在與哈希表中:

若key已經存在,說明該數據命中緩存,則根據key對應的節點地址找到該緩存數據節點,更新該節點的數據值,並將該節點置於雙向鏈表的頭部,同時更新key所對應的節點地址。

若key不存在,說明該數據在緩存中未發生命中,則在雙向鏈表頭部創建新的節點存放新的數據,並在哈希表中添加新的key值與鏈表頭部地址相對應。若鏈表長度大於緩存大小,則刪除鏈表尾部節點以及對應的哈希表中的鍵值對。


每當執行get操作時,先判斷插入的的鍵值對中的key是否存在與哈希表中:

若key已經存在,則可通過key值對應的鏈表中節點的地址,就可取得緩存數據;同時將該節點置於鏈表的頭部並更新key對應的節點地址。

若對應的key不存在於哈希表中,即未發生命中,返回-1。


04 最終實現

說明

list 是C++ STL中容器,底層實現爲雙向循環鏈表,任意位置插入和刪除時間複雜度0(1)。

unordered_map 同爲C++ STL中容器,底層實現爲哈希表。

C++代碼:

class LRUCache {
public:
    int size;
    list<pair<int, int>> cache; 
    unordered_map<int, list<pair<int,int>>::iterator> map;
    
    LRUCache(int capacity) {
        size = capacity;
    }
    
    int get(int key) {
        auto it = map.find(key);  
        if(it == map.end())  //判斷key是否存在於哈希表中
            return -1;      
        auto temp = *map[key];  
        cache.erase(map[key]);  //刪除命中節點
        cache.push_front(temp);  //在鏈表頭部創建新的數據節點 
        map[key] = cache.begin();  //更新key所對應的節點地址
        return temp.second;         
    }
    
    void put(int key, int value) {
        auto it = map.find(key);
        if(it == map.end())
        {
            if(cache.size()==size)  //若緩存已滿
            {
                auto temp = cache.back();  //獲得鏈表尾部節點
                map.erase(temp.first);  //刪除尾部節點對應哈希表鍵值對
                cache.pop_back();  //刪除尾部節點
            }        
            cache.push_front(make_pair(key,value));  //在鏈表頭部插入新的數據節點
            map[key] = cache.begin();  //更新key值對應的節點地址,指向鏈表頭部
        }
        else
        {       
            cache.erase(map[key]);
            cache.push_front(make_pair(key,value));
            map[key] = cache.begin();
        }
    }
};
​
/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

評析:

這種方案的實現實際上是最簡單的一種LRU思想的表現,但是其利用效率不高。在某些情況下,會導致在重複位置的插入和刪除,導致更新效率低下;同時由於哈希表本身的結構也會導致其插入和查詢的效率不穩定。不過理解上述的實現能夠對數據結構的結合和LRU算法有比較明確的瞭解。


ps:個人公衆號【業餘碼農】。裏面有許多校招經驗的分享,還有技術基礎的分享;之後還會分享許多我自己對於互聯網行業的一些看法,有什麼問題也可以在上面問我。感興趣的同學可以關注下。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章