146. 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
提示:
1 <= capacity <= 30000 <= key <= 100000 <= value <= 105- 最多调用
2 * 105次get和put
解决办法
- 先搞懂什么是LRU
- 时间复杂度的要求
什么是LRU
一种页面缓存管理机制,类似图书馆的最近热门读书榜单,热门的书在前面,当有新的热门的书出现时,旧的相较与新书,不太热门的书就会淘汰出榜单
时间复杂度要求
当数据量n时,程序运行时间的“增长速度”有多快。 如果“增长速度”这个词还是有点抽象,你可以把它理解为“工作量的膨胀率”。
假设你是一个搬运工,老板给你安排了任务。而“增长速度”指的不是你搬一趟要多久,而是当任务量翻倍时,你累倒的速度有多快。
- O(1):老板让你去拿一个快递不管快递站里有 10 件快递还是 10,000 件快递,你手里有取件码,进去直接拿了就走。 增长速度: 零增长。即便快递站扩建成了物流园,你拿快递的体力消耗依然是一份。
- O(n):老板让你去数一排苹果,如果桌上有 10 个苹果,你数 10 下;如果桌上有 100 个苹果,你得数 100 下。 增长速度: 线性增长。苹果增加 10 倍,你的体力消耗就增加 10 倍。
用java解决LRU
1. 核心思路:寻找“完美的搭档”
设计LRU的难点在与在O(1)的时间复杂度内同时解决数据的 增、删、查 的操作,核心的思想就是借助高效的数据结构完成目标。且单一的数据结构完全不能满足要求(为什么?),所以我们需要结合两种数据结构完成要求
- hashmap 哈希表: 它的查找效率是O(1),但是它并不是有序的,且无法知道最近使用的时间
- Doubly Linked List 双向链表: 它的插入和删除节点是O(1) (前提是已经知道节点的位置).我们可以约定: 靠近头部的节点是最近使用的,靠近尾部的节点是最久没有使用的。(用这个数据结构来帮助管理)
2.现实的模型

ps: 其实比喻成书榜也是对的
class LRUCache {
class Node {
private int key, value;
private Node prev, next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
public int getValue(){
return this.value;
}
public void setValue(int value){
this.value = value;
}
}
private int size;
private int cap;
private Node head, tail;
private HashMap<Integer, Node> cache;
public LRUCache(int capacity) {
this.cache = new HashMap<Integer, Node>();
this.cap = capacity;
this.size = 0;
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
this.head.next = tail;
this.tail.prev = head;
}
public int get(int key) {
Node node = cache.get(key);
if (null == node) return -1;
rmAndMoveToHead(node);
return node.value;
}
public void put(int key, int value) {
// get
Node node = cache.get(key);
// null?
if(null == node){
// isAdd
node = new Node(key,value);
cache.put(key,node);
size++;
// flush new node index
addNodeTohead(node);
// overCap?
if(size > cap){
// removeOldNode
Node oldNode = tail.prev;
rmNode(oldNode);
cache.remove(oldNode.key);
size--;
// reflush cap;
}
}else{
// not null;
node.setValue(value);
// setNode
cache.put(key,node);
// putItOnTop;
rmAndMoveToHead(node);
}
}
public void addNodeTohead(Node node){
// head => .. => ?
Node top = head.next;
head.next = node;
node.prev = head;
top.prev = node;
node.next = top;
}
public void rmAndMoveToHead(Node node){
rmNode(node);
addNodeTohead(node);
}
public void rmNode(Node node){
// A => node => B
// A => B
Node A = node.prev;
Node B = node.next;
if (A != null) A.next = B;
if (B != null) B.prev = A;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
会出错的点
- map没有set 只有put
- 没有加分号(笑)
- get set 方法需要手写
- addNodeTohead()方法 双向链表 漏掉某个元素的prev 或者next
- 不是,rmNode 为什么要判空啊???
- lruCache.get的时候没有refresh 当前node 的index
- Node 构造方法里不要 Node prev = new Node(0,0); // 为什么不要预设呢??
gemini的总结
- 忘记“读操作”也要刷新: 在 get(key) 时,只返回了值,没有移动节点。
- 变量管理混乱 | 容量与计数器混淆: 把 capacity(上限)当成了 size(当前计数)来做自增。 后果: 导致 if(size > capacity) 的逻辑永远无法正确判断什么时候该执行“淘汰”操作。
- 指针操作错误 (导致 NullPointerException) 孤立节点(断线): 在插入新节点时,只让 head.next 指向新节点,没让 newNode.prev 指向 head。
- HashMap 移除对象错误: cache.remove(node)。 后果: HashMap 的 remove 参数是 Key。传入整个 Node 对象会导致 Map 无法识别,最久的数据其实一直赖在内存里没删掉。
总结:实现 LRU 的“三板斧”
为了不再出错,建议你以后写 LRU 牢记这三个标准步骤:
- 双向链表 + 哨兵: 一定要 head 和 tail 两个假节点,它们能让你永远不用判断 if (node.next == null)。
两把手术刀:
-
removeNode(node):负责把节点从链表里“抠”出来。
-
addToHead(node):负责把节点“贴”到 head 后面。
刷新 = 抠出来 + 贴过去: 无论是 get 命中,还是 put 更新,统一调用 removeNode + addToHead。