146. LRU缓存机制
LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used : 最近最少使用。
重写双向链表 + HashMap [要求 put get 方法时间复杂度为O(1) ]
哈希链表。
问题:怎么保证“get 和 put 方法必须都是 O(1) 的时间复杂度”?
get 查找快 -- hashmap
put 插入快,删除快,有顺序之分 -- 双向链表
问题:“为什么必须要用双向链表”?
因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)
因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。
为了简化,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; } }实现其【 头插 删除 删尾 获得size】 addFirst(Node x); remove(Node x); removeLast(); size();方法。将要实现的这几个需要的 API(这些操作的时间复杂度均为 O(1)。
class DoubleList { // 在链表头部添加节点 x,时间 O(1) public void addFirst(Node x); // 删除链表中的 x 节点(x 一定存在) // 由于是双链表且给的是目标 Node 节点,时间 O(1) public void remove(Node x); // 删除链表中最后一个节点,并返回该节点,时间 O(1) public Node removeLast(); // 返回链表长度,时间 O(1) public int size(); }到这里就能回答刚才“为什么必须要用双向链表”的问题了,因为我们需要删除操作。删除一个节点不光要得到该节点本身的指针,也需要操作其前驱节点的指针,而双向链表才能支持直接查找前驱,保证操作的时间复杂度 O(1)。
有了双向链表的实现,我们只需要在 LRU 算法中把它和哈希表结合起来即可。我们先把逻辑理清楚:
// key 映射到 Node(key, val) HashMap<Integer, Node> map; // Node(k1, v1) <-> Node(k2, v2)... DoubleList cache; int get(int key) { if (key 不存在) { return -1; } else { 将数据 (key, val) 提到开头; return val; } } void put(int key, int val) { Node x = new Node(key, val); if (key 已存在) { 把旧的数据删除; 将新节点 x 插入到开头; } else { if (cache 已满) { 删除链表的最后一个数据腾位置; 删除 map 中映射到该数据的键; } 将新节点 x 插入到开头; map 中新建 key 对新节点 x 的映射; } }这里就能回答之前的问答题“为什么要在链表中同时存储 key 和 val,而不是只存储 val”,注意这段代码:
if (cap == cache.size()) { // 删除链表最后一个数据 Node last = cache.removeLast(); map.remove(last.key); }当缓存容量已满,我们不仅仅要删除最后一个 Node 节点,还要把 map 中映射到该节点的 key 同时删除,而这个 key 只能由 Node 得到。如果 Node 结构中只存储 val,那么我们就无法得知 key 是什么,就无法删除 map 中的键,造成错误。
至此,你应该已经掌握 LRU 算法的思想和实现了,很容易犯错的一点是:处理链表节点的同时不要忘了更新哈希表中对节点的映射。
1. Node类。 int k,v ; Node next,prev; 2. DoubleList类-双向链表类。 int size ; Node head,tail; 3. LRUCache 构造函数。 int cap; DoubleList cache; HashMap<Integer, Node> map; 4. get put 方法。
class LRUCache { // Node 类 class Node { public int key, val; public Node next, prev; public Node(int k, int v) { this.key = k; this.val = v; } } // DoubleList 类 class DoubleList { // 对Node的操作,无 k v 操作 private Node head, tail; private int size; // 双向链表 的实际size大小 public void addFirst(Node node) { if (head == null) { head = tail = node; } else { Node n = head; n.prev = node; node.next = n; head = node; } size++; } public void remove(Node node) { // node tail head if (head == node && tail == node) { head = null; tail = null; } else if (tail == node) { // 最后 前下=null ; node.pre.next node.prev.next = null; tail = node.prev; } else if (head == node) { // 开头 下前=null ; node.next.pre node.next.prev = null; head = node.next; } else { node.prev.next = node.next; node.next.prev = node.prev; } size--; } public Node removeLast() { Node node = tail; remove(tail); // 调用了remove() return node; } public int size() { return size; } } // LRUCache 构造函数 private int cap; // 双向链表 容量 capacity private DoubleList cache; private HashMap<Integer, Node> map; // map 的 k , v public LRUCache(int capacity) { this.cap = capacity; cache = new DoubleList(); map = new HashMap<>(); } // get put 方法 public int get(int key) { if(!map.containsKey(key)) return -1; int val = map.get(key).val; put(key, val); // 此处调用了 put() return val; } public void put(int key, int value) { Node x = new Node(key, value); if (map.containsKey(key)){ cache.remove(map.get(key)); cache.addFirst(x); map.put(key,x); } else { if (cap == cache.size()) { Node last = cache.removeLast(); map.remove(last.key); } cache.addFirst(x); map.put(key,x); } } }对put()的理解
put 方法主要有3个
1.remove 删除缓存cache或者删除缓存cache、map中的数据
2.缓存头插 cache.addFirst(x);
3.更新map map.put(key,x);
那么索性如下写法
public void put(int key, int value) { Node x = new Node(key, value); if (map.containsKey(key)){ cache.remove(map.get(key)); } else { if (cap == cache.size()) { Node last = cache.removeLast(); map.remove(last.key); } } cache.addFirst(x); map.put(key,x); }
写在最后,好久没白嫖别人思想了,还好自己得空实现了一版代码,此处奉上链接,以示敬意。
参考
https://leetcode-cn.com/problems/lru-cache/solution/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/