知多一點 LRU 緩存算法

hello~親愛的觀衆老爺們你們好~最近沉迷 GraphQL 沒法自拔,使用的過程當中接觸到很多的緩存機制,LRU 算法是比較經常使用的一種,於是對此產生了興趣。正好以前刷 LeetCode 時完成了這答題,查閱了相關資料後翻看當初的實現,才知道以前是多蠢~於是有了這篇文章,記錄下這個算法的思路。javascript

本文主要介紹 LRU 緩存算法相關,並提供一個實現的思路。除了讓你們知多一點 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) 時間複雜度內完成這兩種操做?數組

實現固然要完美,於是須要實現進階的要求~緩存

LRU 算法實現思路

算法其實能夠拆分爲三個方面去考慮,分別是獲取、寫入與淘汰。先考慮最簡單的一方面:獲取。數據結構

如須要在 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(1) 時間複雜度,完成插入與刪除操做的數據結構。

有沒有這樣的數據結構呢?有,那就是雙向鏈表!鏈表在插入與刪除操做上,都是 O(1) 時間的複雜度,但查找某個元素比較麻煩,是 O(n) 。然而哈希表的存在彌補了缺陷,查找元素的簡直垂手可得!只要修改哈希表,將存儲的值設爲鏈表節點便可。

LRU 算法實現

有了思路,實現起來就至關簡單了,此處直接貼一下所有代碼:

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 算法還有其餘實現方法,只是這種方法清晰易懂,效率也高,於是選用了這種思路。上面的代碼也並不是完美,好比沒將操做鏈表相關的代碼獨立出去,但爲了易於理解及節省篇幅,就跳過了。

事實上,前端是比較少用到算法和數據結構的,但不表明它們沒有用。在以前的解題中,我就是使用數組排序的方法完成這道題,效率是至關低下。熟悉數據結構,有意識地在實際場景中使用它們,除了能解決問題以外,我的也會獲得很大的提高。

以上是我的的一點淺見,感謝各位看官大人看到這裏。知易行難,但願本文對你有所幫助~謝謝!

相關文章
相關標籤/搜索