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; } }