LeetCode100道面试必会算法-LRU缓存-01
LRU缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
解题思路
-
使用HashMap存储键值对,其中键是缓存的键,值是指向双向链表节点的引用。这样可以通过键快速找到对应的节点。
-
使用双向链表来存储缓存的键值对,通过节点之间的前后连接来维护LRU顺序。链表的头部表示最近使用的节点,尾部表示最久未使用的节点。
-
LRUCache类中的属性包括HashMap和双向链表的头尾节点以及缓存的容量。
-
在get方法中,如果缓存中存在指定的键,则将对应节点移动到链表头部,并返回对应的值;如果不存在,则返回-1。
-
在put方法中,如果缓存中存在指定的键,则更新对应节点的值,并将该节点移动到链表头部;如果不存在,则创建新节点并加入到HashMap和链表头部。如果缓存已满,则需要淘汰最久未使用的节点,并从HashMap中删除对应的键值对。
-
cutNode方法用于从双向链表中切断指定节点的连接,实现移除节点的功能。
-
cutTail方法用于切断尾部节点的连接,实现移除最久未使用的节点,并返回被移除的节点。
-
insertHead方法用于将指定节点插入到链表头部。
-
Node类是LRUCache内部的私有类,用于表示双向链表的节点,包含key、value、前驱节点和后继节点等属性。
实现代码+详细注释
class LRUCache { // 哈希表用于存储键值对,键是缓存的键,值是指向双向链表节点的引用 HashMap<Integer, Node> map = new HashMap<>(); int capacity; // 缓存的容量 Node head, tail; // 双向链表的头节点和尾节点 // 构造函数,初始化LRU缓存 public LRUCache(int _capacity) { capacity = _capacity; head = new Node(-1, -1); // 头节点 tail = new Node(-1, -1); // 尾节点 head.next = tail; // 头节点的下一个指向尾节点 tail.pre = head; // 尾节点的前一个指向头节点 } // 获取指定key对应的值 public int get(int key) { if (map.containsKey(key)) { Node node = map.get(key); // 获取对应节点 cutNode(node); // 将节点从当前位置断开 insertHead(node); // 将节点移动到头部 return node.val; // 返回节点值 } return -1; // 如果不存在,返回-1 } // 插入或更新键值对 public void put(int key, int value) { if (map.containsKey(key)) { // 如果key已经存在于缓存中 Node node = map.get(key); // 获取对应节点 node.val = value; // 更新节点值 cutNode(node); // 将节点从当前位置断开 insertHead(node); // 将节点移动到头部 } else { Node node = new Node(key, value); // 创建新节点 map.put(key, node); // 将节点加入到HashMap中 insertHead(node); // 将节点移动到头部 if (map.size() > capacity) { // 如果缓存已满 Node delNode = cutTail(); // 移除最久未使用的节点 map.remove(delNode.key); // 从HashMap中移除对应键值对 } } } // 将节点从双向链表中断开连接 private void cutNode(Node node) { node.pre.next = node.next; // 前一个节点的下一个指向下一个节点 node.next.pre = node.pre; // 下一个节点的前一个指向前一个节点 node.pre = null; // 断开前驱节点连接 node.next = null; // 断开后继节点连接 } // 移除尾部节点,并返回被移除的节点 private Node cutTail() { Node last = tail.pre; // 获取尾部节点的前一个节点(最久未使用的节点) last.pre.next = tail; // 前一个节点的下一个指向尾节点 tail.pre = last.pre; // 尾节点的前一个指向前一个节点 last.pre = null; // 断开前驱节点连接 last.next = null; // 断开后继节点连接 return last; // 返回被移除的节点 } // 将节点插入到双向链表的头部 private void insertHead(Node node) { Node next = head.next; // 获取头部节点的下一个节点 node.next = next; // 新节点的下一个指向头部节点的下一个节点 node.pre = head; // 新节点的前一个指向头部节点 next.pre = node; // 头部节点的下一个节点的前一个指向新节点 head.next = node; // 头部节点的下一个指向新节点 } // 双向链表节点类 class Node { int key; // 键 int val; // 值 Node next; // 后继节点 Node pre; // 前驱节点 // 构造函数 public Node(int _key, int _val) { key = _key; val = _val; } } }
反思:遇到题要冷静思考,有个大概的思路,链表题目注意一定要捋清逻辑,别把自己绕晕了。常见方法就是舍得用变量,比如虚拟头尾节点