LRU 缓存淘汰算法就是一种常用策略, LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
1、如果我们每次默认从链表尾部添加元素,那么显然越靠尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的。
2、对于某一个 key
,我们可以通过哈希表快速定位到链表中的节点,从而取得对应 val
。
3、链表显然是支持在任意位置快速插入和删除的,改改指针就行。只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助哈希表,可以通过 key
快速映射到任意一个链表节点,然后进行插入和删除。
代码实现:
节点:
class Node { public int key, val; public Node next, prev; public Node(int k, int v) { this.key = k; this.val = v; } }
双向链表:
class DoubleList { // 头尾虚节点 private Node head, tail; // 链表元素数 private int size; public DoubleList() { // 初始化双向链表的数据 head = new Node(0, 0); tail = new Node(0, 0); head.next = tail; tail.prev = head; size = 0; } // 在链表尾部添加节点 x,时间 O(1) public void addLast(Node x) { x.prev = tail.prev; x.next = tail; tail.prev.next = x; tail.prev = x; size++; } // 删除链表中的 x 节点(x 一定存在) // 由于是双链表且给的是目标 Node 节点,时间 O(1) public void remove(Node x) { x.prev.next = x.next; x.next.prev = x.prev; size--; } // 删除链表中第一个节点,并返回该节点,时间 O(1) public Node removeFirst() { if (head.next == tail) return null; Node first = head.next; remove(first); return first; } // 返回链表长度,时间 O(1) public int size() { return size; } }
调用:
class LRUCache { // key -> Node(key, val) private HashMap<Integer, Node> map; // Node(k1, v1) <-> Node(k2, v2)... private DoubleList cache; // 最大容量 private int cap; public LRUCache(int capacity) { this.cap = capacity; map = new HashMap<>(); cache = new DoubleList(); } public int get(int key) { if (!map.containsKey(key)) { return -1; } // 将该数据提升为最近使用的 makeRecently(key); return map.get(key).val; } public void put(int key, int val) { if (map.containsKey(key)) { // 删除旧的数据 deleteKey(key); // 新插入的数据为最近使用的数据 addRecently(key, val); return; } if (cap == cache.size()) { // 删除最久未使用的元素 removeLeastRecently(); } // 添加为最近使用的元素 addRecently(key, val); } /* 将某个 key 提升为最近使用的 */ private void makeRecently(int key) { Node x = map.get(key); // 先从链表中删除这个节点 cache.remove(x); // 重新插到队尾 cache.addLast(x); } /* 添加最近使用的元素 */ private void addRecently(int key, int val) { Node x = new Node(key, val); // 链表尾部就是最近使用的元素 cache.addLast(x); // 别忘了在 map 中添加 key 的映射 map.put(key, x); } /* 删除某一个 key */ private void deleteKey(int key) { Node x = map.get(key); // 从链表中删除 cache.remove(x); // 从 map 中删除 map.remove(key); } /* 删除最久未使用的元素 */ private void removeLeastRecently() { // 链表头部的第一个元素就是最久未使用的 Node deletedNode = cache.removeFirst(); // 同时别忘了从 map 中删除它的 key int deletedKey = deletedNode.key; map.remove(deletedKey); } }