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