(链表:哈希表 + 双向链表)146.LRU 缓存
题目
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
实现 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
提示:
1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
最多调用 2 * 105 次 get 和 put
思路
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
- 双向链表:最近使用的发到链表头,按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
- 哈希表:为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。作用:快速定位节点,和链表保持数据一致性,确保淘汰过程正确,控制容量
get和put操作流程
get流程:
- 判断key是否存在,不存在则返回-1
- key存在,难到这个key的节点Node,并将当前这个Node移动到链表的头部
- 返回Node结点的value
put流程:
- 判断key是否存在哈希表中,不存在,则使用key和calue创建应该新的节点Node,并将这个Node添加到链表的头部,再判断链表中的节点数是否超过容量,超出则删除链表尾部节点Node 和 哈希表中对应的项
- 如果key存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
通过分析上面的流程分析,我们需要定义几个数据结构和方法
- 节点Node的定义:使用双向链表
- 优点:快速移动到头节点,快速删除尾部节点,维护访问顺序
- 代码:
class DLinkedNode {int key;int value;// 前节点DLinkedNode prev;// 后节点DLinkedNode next;// 无参构造public DLinkedNode() {}// 构造public DLinkedNode(int _key, int _value) {key = _key; value = _value;} }
- 删除节点:
private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev; }
- 删除尾节点:
private DLinkedNode removeTail() {DLinkedNode res = tail.prev;removeNode(res);return res;
}
- 新增节点:在头部添加
private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node; }
- Node移动到头部的操作:removeToHead
private void moveToHead(DLinkedNode node) {removeNode(node);addToHead(node); }
- LRUCache参数:初始化容量、实时容量、map缓存、头尾节点
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>(); // 实时记录缓存元素数量:跟踪当前缓存中的数据量,用于容量控制 private int size; // 容量阈值:定义缓存最大承载量 private int capacity; // head:表操作锚点:作为双向链表的固定起始点,简化头部插入操作 // tail:LRU节点标识:标记链表末端,便于快速定位待淘汰节点 private DLinkedNode head, tail;
- 初始化zhegLRUCache
public LRUCache(int capacity) {this.size = 0;this.capacity = capacity;// 使用伪头部和伪尾部节点head = new DLinkedNode();tail = new DLinkedNode();head.next = tail;tail.prev = head; }
- get操作
public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// 如果 key 存在,先通过哈希表定位,再移到头部moveToHead(node);return node.value; }
- put 操作
public void put(int key, int value) {DLinkedNode node = cache.get(key);if (node == null) {// 如果 key 不存在,创建一个新的节点DLinkedNode newNode = new DLinkedNode(key, value);// 添加进哈希表cache.put(key, newNode);// 添加至双向链表的头部addToHead(newNode);++size;if (size > capacity) {// 如果超出容量,删除双向链表的尾部节点DLinkedNode tail = removeTail();// 删除哈希表中对应的项cache.remove(tail.key);--size;}}else {// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部node.value = value;moveToHead(node);} }
算法
public class LRUCache {class DLinkedNode {int key;int value;DLinkedNode prev;DLinkedNode next;public DLinkedNode() {}public DLinkedNode(int _key, int _value) {key = _key; value = _value;}}private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();private int size;private int capacity;private DLinkedNode head, tail;public LRUCache(int capacity) {this.size = 0;this.capacity = capacity;// 使用伪头部和伪尾部节点head = new DLinkedNode();tail = new DLinkedNode();head.next = tail;tail.prev = head;}public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// 如果 key 存在,先通过哈希表定位,再移到头部moveToHead(node);return node.value;}public void put(int key, int value) {DLinkedNode node = cache.get(key);if (node == null) {// 如果 key 不存在,创建一个新的节点DLinkedNode newNode = new DLinkedNode(key, value);// 添加进哈希表cache.put(key, newNode);// 添加至双向链表的头部addToHead(newNode);++size;if (size > capacity) {// 如果超出容量,删除双向链表的尾部节点DLinkedNode tail = removeTail();// 删除哈希表中对应的项cache.remove(tail.key);--size;}}else {// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部node.value = value;moveToHead(node);}}private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;}private void moveToHead(DLinkedNode node) {removeNode(node);addToHead(node);}private DLinkedNode removeTail() {DLinkedNode res = tail.prev;removeNode(res);return res;}
}
但是在日常工作中,还是直接使用LinkedHashMap便可以了
-
插入模式(默认): 新节点追加至链表尾部
示例插入顺序 A → B → C → D
-
访问模式
- accessOrder=true:访问/插入节点均移至尾部,LRU缓存淘汰策略
- (accessOrder=true): 被访问节点移至链表尾部
访问B后的顺序 A → C → D → B
- (accessOrder=true): 被访问节点移至链表尾部
- 默认值:新节点始终追加尾部,保留原始插入顺序
- accessOrder=true:访问/插入节点均移至尾部,LRU缓存淘汰策略
class LRUCache extends LinkedHashMap<Integer, Integer>{private int capacity;public LRUCache(int capacity) {// accessOrder=truesuper(capacity, 0.75F, true);this.capacity = capacity;}public int get(int key) {return super.getOrDefault(key, -1);}public void put(int key, int value) {super.put(key, value);}@Overrideprotected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {// 触发淘汰最久未使用项return size() > capacity; }
}