题目
请你设计并实现一个满足 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=true
super(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);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
// 触发淘汰最久未使用项
return size() > capacity;
}
}