hello~親愛的觀衆老爺們你們好~最近沉迷 GraphQL 沒法自拔,使用的過程當中接觸到很多的緩存機制,LRU 算法是比較經常使用的一種,於是對此產生了興趣。正好以前刷 LeetCode 時完成了這答題,查閱了相關資料後翻看當初的實現,才知道以前是多蠢~於是有了這篇文章,記錄下這個算法的思路。javascript
本文主要介紹 LRU 緩存算法相關,並提供一個實現的思路。除了讓你們知多一點 LRU 算法以外,但願整個解決問題的思路,能幫助各位在其餘類似的場景中解決問題~如下是正文:前端
LRU 算法是緩存淘汰算法的一種,而 LRU 是 Least Recently Used 三個單詞的縮寫。簡單地說,因爲 內存空間有限,須要根據某種策略淘汰不那麼重要的數據,用以釋放內存。LRU 的策略是最先操做過的數據放最後,最晚操做過的放開始,按操做時間逆序,若是達到上限,則淘汰末尾的項。java
整個 LRU 算法有必定的複雜度,擴展起來能夠增添許多功能,生產環境中建議直接使用成熟的庫,如 lru-cache。而接下來將帶來一個簡單的實現,也是這個算法實現的骨架。node
既然是算法,那必須先定義待解決的問題,此處參考 LeetCode 上的 146. LRU緩存機制。git
運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持如下操做: 獲取數據 get 和 寫入數據 put 。獲取數據 get(key) - 若是密鑰 (key) 存在於緩存中,則獲取密鑰的值(老是正數),不然返回 -1。 寫入數據 put(key, value) - 若是密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據以前刪除最近最少使用的數據值,從而爲新的數據值留出空間。github
進階:算法
你是否能夠在 O(1) 時間複雜度內完成這兩種操做?數組
實現固然要完美,於是須要實現進階的要求~緩存
算法其實能夠拆分爲三個方面去考慮,分別是獲取、寫入與淘汰。先考慮最簡單的一方面:獲取。數據結構
如須要在 O(1) 的時間複雜度中完成獲取操做,那哈希表是一個很好的選擇。JavaScript 的實現十分簡單,使用對象便可,若是 key
不是簡單類型,可使用 Map
實現。按算法須要解決問題的場景,此處使用對象便可:
var LRUCache = function(capacity) {
...
this.map = {};
...
};
複製代碼
既然使用了哈希表,獲取數據天然也能是 O(1)。
最麻煩是淘汰。儘管在哈希表中刪除數據,時間複雜度也是常數,但咱們沒法得知該刪除哪項。那修改哈希表中存儲的 value
,從直接存儲 value
改成存儲一個對象,除了相關的值以外,再加上修改時間,這是否可行?
儘管有了時間,但淘汰是發生在合適呢?它發生在寫入新數據之時,一旦須要淘汰數據,則須要遍歷整個哈希表以獲取最先操做的那一項。獲取操做再也不是 O(1) 時間複雜度。此路不通~
純哈希表是完成不了這需求的,那麼空間換時間怎樣~用額外的變量,記錄最先操做的那一項,須要淘汰時直接淘汰該項。接近一點目標,但仍然不行。考慮這個場景,先操做 A
,再操做 B
,最後再操做 A
。如須要淘汰一項,那須要淘汰的是 B
,然而變量記錄的是 A
,不符合需求。
那不記錄一項,用數組記錄所有的項怎樣,每次操做某項數據,就將這一項從數組中取出,再 push
進數組。再接近一點目標,但爲了找到這一項,須要遍歷數組,時間複雜度是 O(n)。
儘管上述的路達不到目標,但仍是有收穫:
有沒有這樣的數據結構呢?有,那就是雙向鏈表!鏈表在插入與刪除操做上,都是 O(1) 時間的複雜度,但查找某個元素比較麻煩,是 O(n) 。然而哈希表的存在彌補了缺陷,查找元素的簡直垂手可得!只要修改哈希表,將存儲的值設爲鏈表節點便可。
有了思路,實現起來就至關簡單了,此處直接貼一下所有代碼:
const LRUCache = function(capacity) {
this.map = {};
this.size = 0;
this.maxSize = capacity;
// 鏈表的頭
this.head = {
prev: null,
next: null
};
// 鏈表的尾
this.tail = {
prev: this.head,
next: null
};
this.head.next = this.tail;
};
LRUCache.prototype.get = function(key) {
if (this.map[key] !== undefined) {
// 將對應的節點抽出並設爲鏈表的首項並返回對應的值
const node = this.extractNode(this.map[key]);
this.insertNodeToHead(node);
return this.map[key].val;
} else {
return -1;
}
};
LRUCache.prototype.put = function(key, value) {
let node;
if (this.map[key]) {
// 如若該項存在,則抽取出來並設置爲對應的值
node = this.extractNode(this.map[key]);
node.val = value;
} else {
// 如該項不存在,那就創造一個新節點
node = {
prev: null,
next: null,
val: value,
key,
};
this.map[key] = node;
this.size++;
}
// 將節點設爲鏈表的首項
this.insertNodeToHead(node);
if (this.size > this.maxSize) {
// 超過限制則刪除最後一項
const delNode = this.tail.prev;
const delKey = delNode.key;
this.extractNode(delNode);
this.size--;
delete this.map[delKey];
}
};
// 插入節點到鏈表首項
LRUCache.prototype.insertNodeToHead = function(node) {
const head = this.head;
const oldFirstNode = this.head.next;
node.prev = head;
head.next = node;
node.next = oldFirstNode;
oldFirstNode.prev = node;
return node;
}
// 從鏈表中抽取節點
LRUCache.prototype.extractNode = function(node) {
const before = node.prev;
const after = node.next;
before.next = after;
after.prev = before;
node.prev = null;
node.next = null;
return node;
}
複製代碼
重要的地方都加了註釋,根據上面的思路,相信你必定明白上面的代碼~能夠看到,整個實現沒有循環,於是全部操做的時間複雜度都可視爲 O(1)。
以上就是本文的所有內容啦!其實 LRU 算法還有其餘實現方法,只是這種方法清晰易懂,效率也高,於是選用了這種思路。上面的代碼也並不是完美,好比沒將操做鏈表相關的代碼獨立出去,但爲了易於理解及節省篇幅,就跳過了。
事實上,前端是比較少用到算法和數據結構的,但不表明它們沒有用。在以前的解題中,我就是使用數組排序的方法完成這道題,效率是至關低下。熟悉數據結構,有意識地在實際場景中使用它們,除了能解決問題以外,我的也會獲得很大的提高。
以上是我的的一點淺見,感謝各位看官大人看到這裏。知易行難,但願本文對你有所幫助~謝謝!