LRU原理
假設內存只能容納3個頁大小,按照 7 0 1 2 0 3 0 4 的次序訪問頁。假設內存按照棧的方式來描述訪問時間,在上面的,是最近訪問的,在下面的是,最遠時間訪問的,LRU就是這樣工作的。
但是如果讓我們自己設計一個基於 LRU 的緩存,這樣設計可能問題很多,這段內存按照訪問時間進行了排序,會有大量的內存拷貝操作,性能差。
那麼如何設計一個LRU緩存,使得放入和移除都是 O(1) ?
我們需要把訪問次序維護起來,但是不能通過內存中的真實排序來反應,有一種方案就是使用雙向鏈表。
基於 HashMap 和 雙向鏈表實現 LRU 算法
設計思路
使用 HashMap 存儲 key,使得 save 和 get key的時間複雜度都是 O(1),將HashMap 的 value 指向雙向鏈表實現的 LRU 的 Node 節點。如下圖所示:
原理演示
LRU 存儲是基於雙向鏈表實現的。其中 head 代表表頭,tail 代表尾部。預先設置 LRU 的容量,若存滿,可通過 O(1) 的時間淘汰掉雙向鏈表的尾部。每次新增和訪問數據,把新的節點增加到鏈表頭部(都可以通過 O(1)的效率),或者把已經存在的節點移動到鏈表頭部。
下面展示了capacity=3 的LRU,在存儲和訪問過程中的變化。爲了簡化圖複雜度,圖中沒有展示 HashMap部分的變化,僅僅演示了上圖 LRU 雙向鏈表的變化。
我們對這個LRU緩存的操作序列如下:
save("key1", 7)
save("key2", 0)
save("key3", 1)
save("key4", 2)
get("key2")
save("key5", 3)
get("key2")
save("key6", 4)
相應的 LRU 雙向鏈表部分變化如下:
核心步驟總結
- save(key, value)——首先在 HashMap 找到 key 對應的節點,(a)如果節點存在,更新節點的值,並把這個節點移動到隊頭。(b)如果不存在,需構造新的節點,並且嘗試把節點塞到隊頭。(c)如果LRU空間不足,則通過 tail 淘汰掉隊尾的節點,同時在 HashMap 中移除 key。
- get(key)——通過 HashMap 找到 LRU 鏈表節點,因爲根據LRU 原理,這個節點是最新訪問的,所以要把節點插入到隊頭,然後返回緩存的值。
-
完整基於 Java 的代碼參考如下
class DLinkedNode { String key; int value; DLinkedNode pre; DLinkedNode post; }
LRU Cache
public class LRUCache { private Hashtable<Integer, DLinkedNode> cache = new Hashtable<Integer, DLinkedNode>(); private int count; private int capacity; private DLinkedNode head, tail; public LRUCache(int capacity) { this.count = 0; this.capacity = capacity; head = new DLinkedNode(); head.pre = null; tail = new DLinkedNode(); tail.post = null; head.post = tail; tail.pre = head; } public int get(String key) { DLinkedNode node = cache.get(key); if(node == null){ return -1; // should raise exception here. } // move the accessed node to the head; this.moveToHead(node); return node.value; } public void set(String key, int value) { DLinkedNode node = cache.get(key); if(node == null){ DLinkedNode newNode = new DLinkedNode(); newNode.key = key; newNode.value = value; this.cache.put(key, newNode); this.addNode(newNode); ++count; if(count > capacity){ // pop the tail DLinkedNode tail = this.popTail(); this.cache.remove(tail.key); --count; } }else{ // update the value. node.value = value; this.moveToHead(node); } } /** * Always add the new node right after head; */ private void addNode(DLinkedNode node){ node.pre = head; node.post = head.post; head.post.pre = node; head.post = node; } /** * Remove an existing node from the linked list. */ private void removeNode(DLinkedNode node){ DLinkedNode pre = node.pre; DLinkedNode post = node.post; pre.post = post; post.pre = pre; } /** * Move certain node in between to the head. */ private void moveToHead(DLinkedNode node){ this.removeNode(node); this.addNode(node); } // pop the current tail. private DLinkedNode popTail(){ DLinkedNode res = tail.pre; this.removeNode(res); return res; } }