天天看點

LRU算法 原理及實作代碼實作

LRU原理

LRU(Least recently used,最近最少使用)算法根據資料的曆史通路記錄來進行淘汰資料,其核心思想是“如果資料最近被通路過,那麼将來被通路的幾率也更高”。

  • 最常見的實作是使用一個連結清單儲存緩存資料,詳細算法實作如下
LRU算法 原理及實作代碼實作
  • 新資料插入到連結清單頭部;
  • 每當緩存命中(即緩存資料被通路),則将資料移到連結清單頭部;
  • 當連結清單滿的時候,将連結清單尾部的資料丢棄。

    【命中率】

    當存在熱點資料時,LRU的效率很好,但偶發性的、周期性的批量操作會導緻LRU命中率急劇下降,緩存污染情況比較嚴重。

    【複雜度】

    實作簡單。

    【代價】

    命中時需要周遊連結清單,找到命中的資料塊索引,然後需要将資料移到頭部。

代碼實作

很多程式設計語言都有内置的哈希連結清單或者類似 LRU 功能的庫函數,但是為了幫大家了解算法的細節,我們用 Java 自己造輪子實作一遍 LRU 算法。

首先,我們把雙連結清單的節點類寫出來,為了簡化,key 和 val 都認為是 int 類型:

class Node {
    public int key, val;
    public Node next, prev;
    public Node(int k, int v) {
        this.key = k;
        this.val = v;
    }
}
           

然後依靠我們的 Node 類型建構一個雙連結清單,實作幾個要用到的 API,這些操作的時間複雜度均為 O(1) :

class DoubleList {  
    // 在連結清單頭部添加節點 x
    public void addFirst(Node x);

    // 删除連結清單中的 x 節點(x 一定存在)
    public void remove(Node x);

    // 删除連結清單中最後一個節點,并傳回該節點
    public Node removeLast();

    // 傳回連結清單長度
    public int size();
}
           

PS:這就是普通雙向連結清單的實作,為了讓讀者集中精力了解 LRU 算法的邏輯,就省略連結清單的具體代碼。

到這裡就能回答剛才“為什麼必須要用雙向連結清單”的問題了,因為我們需要删除操作。删除一個連結清單節點不光要得到該節點本身的指針,也需要操作其前驅節點的指針,而雙向連結清單才能支援直接查找前驅,保證操作的時間複雜度 O(1)。

有了雙向連結清單的實作,我們隻需要在 LRU 算法中把它和哈希表結合起來即可。我們先把邏輯理清楚:

LRU算法 原理及實作代碼實作

如果能夠看懂上述邏輯,翻譯成代碼就很容易了解了:

LRU算法 原理及實作代碼實作