十分鐘看懂JS的LRU Cache 算法(下)

上文介紹了LRU Cache的場景點擊回顧,以及在es6前提下能夠藉助Map結構來解決,而本文將介紹在es5條件下,更加根正苗紅不取巧的解決方案。javascript

image.png

仍是簡單介紹下場景要求,方便沒看前一篇的同窗也能直接看(順便湊點字數):前端

當用戶訪問不一樣站點時,瀏覽器須要緩存在對應站點的一些信息,當下次訪問同一個站點的時候,經過讀取緩存就能夠實現更快速的訪問。緩存的分配空間是有限的,因此當空間不足時,須要優先刪除最近不常用的數據,實現緩存的管理。

需求整理

把上述場景問題進一步整理成代碼需求:請實現一個class,提供如下功能:java

  1. 提供put方法,用於寫入不一樣的緩存數據,假設每條數據形式是{'域名','info'},例如{'https://segmentfault.com': '一些關鍵信息'}(若是是同一域名重複寫入,則新寫入覆蓋舊數據);
  2. 當緩存達到上限時, 調用put寫入緩存以前, 要刪除最近最少使用的數據
  3. 提供get方法,用於讀取緩存數據,同時須要把被讀取的數據,移動到最近使用數據 ;
  4. 考慮到性能問題,但願getset的操做的複雜度是O(1)(簡單理解就是,不能使用遍歷)

數據選型

一樣的,先考慮數據結構的選擇,既然是es5那麼可選的只有ArrayObject了,array一般用於須要表示順序的數據結構,可是從上一篇文章咱們能夠知道,算法實現的核心,在於維持新插入的數據排在後面,舊數據放在前面的順序,因此每次讀取數據以後須要重排序,來維持這個順序。而用數組存放數據的話,排序必定躲不開遍歷,那就不符合前面的第四條,因此只能考慮Object,然而Object如何表示順序呢? 就必需要用到鏈表結構了。node

鏈表介紹

基本內容

考慮到本篇文章的部分讀者有可能第一次接觸鏈表結構,仍是在這裏作一下簡要的介紹,熟悉的讀者能夠跳過本節。es6

image.png

鏈表結構如上圖所示,是由一些節點以及節點之間的指針串聯起來的。(顏色是爲了方便區分屬於哪一個節點的指針)面試

  • A`BCD屬於常規節點,HeadTail`是虛擬的頭部和尾部節點,這是爲了方便找到鏈表的首末設定的;
  • 對於每一個節點而言,它只會記住它的先後位置(若是是單向鏈表,就只須要記住一個方向;若是是雙向鏈表,就須要分別記住前面和後面的節點,上圖是雙向鏈表)並用prenext指針來訪問;

Head節點 的preTail節點next都指向null算法

操做圖解

仍是以雙向鏈表爲例,從末尾增長節點時如圖所示,只需改動Tail以及實際末尾節點(本例中是D)的指針便可(關注紅色線框標的節點便可):編程

image.png

刪除節點則如圖(以c節點爲例)只須要把該節點先後節點鏈接,而且把自身的兩個指針指向null便可:segmentfault

image.png

移動節點的方式一樣借用前文裏」移動節點等於先刪除後從新從首位插入「這個思路。數組

image.png

如圖所示,假設C節點 被get方法讀取,那麼須要把C節點移到鏈表最前端,實現從左到右的變化,看似複雜,實際上只要執行如下僞代碼:

// 將節點移動到首位
moveToHead(node) {
  if(node){
     // 把B和D連起來
    node.pre.next  = node.next;// B的next指針指向D
    node.next.pre = node.pre; // D的nex指針指向B
    
    // 把C節點移動到head和A之間 
    head.next.pre = node; // A節點的pre指針 指向C
    node.next = head.next; // C節點的next指針 指向A
    node.pre = head;  //  C節點的pre指針 指向head
    head.next = node.pre; // head的next指針 指向C
  }
}

其實就是修改目標節點(C)的先後指針head節點的先後指針,以及目標節點先後節點(B和D)的先後指針(最多就涉及到5個節點,這是固定的,因此複雜度只是O(1))。

上面步驟只是解決了重排序的複雜度問題,可是還須要處理get讀取時O(1)複雜度的問題,鏈表結構方便排序,可是讀取難度較大,因此同時咱們還要維護一個hash map(哈希表),在es5下用objec能夠實現,那麼整個算法的難點基本完成,能夠分步寫代碼了。

算法實現

首先,提供鏈表節點的類,結構就是domaininfo以外,再加一個pre指針和一個next指針:

function DoubleLinkNode (domain, info){
    this.doamin = domain;
    this.info = info;
    this.pre = null;
    this.next = null;
}

其次,是LRU Cache的構造函數:

function LRUCache (size){
    this.size = size;
    this.hashMap = {};
    // 初始化虛擬的頭尾節點 方便找到鏈表頭尾
    this.head = new DoubleLinkNode();
    this.tail = new DoubleLinkNode();
    this.head.next = this.tail;
    this.tail.pre = this.head;
}

而後同樣的是先寫put方法,

LRUCache.prototype.put = function (domain, info){
   // 首先判斷節點是否存在,存在則更新對應信息,不存在則插入
   if(this.hashmap[domain]){
     const node = this.hashmap[domian];
     node.info = info;// 更新
     this.moveToTop(node); // todo1 將節點移動到最前面
     return ;
   } 
   // 不然插入新節點
   const size = Object.keys(this.hashmap).length;
   if(size >= this.size)}{
       // 超過容量,須要先刪除最不常用的節點,也就是末尾節點
       const node = this.tail.pre;
       this.removeNode(node); // todo2 將節點移除
   }
   // 正常插入新節點 並添加到最前面
   const newNode =  new DoubleLinkNode(domain, info);
   this.hashMap[domain] = info;
   this.moveToTop(node);
};

爲了閱讀清晰,能夠提取moveToTopdeleteNode方法,接下來補上實現,因爲前面說過思路了,也比較簡單:

LRUCache.prototype.moveToTop = function (node){
    head.next.pre = node;
    node.next = head.next;
    node.pre = head; 
    head.next = node;
};
LRUCache.prototype.deleteNode = funtion(node) {
    // 鏈表中移除節點實際上就是將節點的先後節點相連 孤立目標節點便可
    node.prev.next = node.next;
    node.next.prev = node.prev;
    node.prev = null;
    node.next = null; 
    
    // 別忘了還要從哈希表去掉節點的key值
    delete this.hashMap[node.domain];
   
}

最後是get方法, 也比較簡單:

LRUCache.prototype.get(domain) = function(){
    if (!this.hashmap[domain]) {
      return false;
    }
    const node = this.hashmap[domain];
    this.deleteNode(node)
    // 由於deleteNode的時候刪除了 因此要從新登記
    this.hashmap[domain] = node;
    this.moveToTop(node);
    return node.info;
};

小結

其實雙向鏈表的思路相對前一篇的map實現更有普適性,這個思路不只適用於js,在C語言和其餘語言同樣能夠實現。並且,面試也能夠拿來吹牛逼(劃重點 面試)。

關於鏈表結構,有算法基礎的同窗可能比較熟悉,可是對於第一次接觸的同窗同窗比較陌生,因此考慮再三仍是決定寫的詳細一些,力求每一篇文章都儘可能讓讀者讀起來不至於太費勁,更貼合本身寫博客的初衷。

可能算法類和源碼類的文章你們都不是很喜歡(固然也可能僅僅是這類文章我寫的不夠容易讀懂,或者你們以爲這是屠龍技,並不實用),相對而言面經和實踐知識點的文章更受歡迎,畢竟人都是有畏難情緒的,不過我以爲仍是能夠對這些內容都作一些瞭解,學習算法思路,一方面對於找工做的朋友短時間就有幫助,可是對於長期從事編程行業來講,也可以增加知識,鍛鍊邏輯能力。

圖片

總結

但願你們對於喜好的文章,可以點贊和收藏,這樣也能必定程度上給我個反饋,哪些文章寫的較好,哪些文章還有不足,或者對於行文風格和內容有任何意見的,都歡迎私信交流。

最後依然是慣例,RingCentral目前在杭州也設置了辦公點,並且能夠申請長期遠程辦公,告別996,工做生活兩不誤,有興趣的同窗能夠私信諮詢(主頁有聯繫方式),能夠免費幫忙內推~

相關文章
相關標籤/搜索