大家好,我是前端开发者,146. LRU 缓存 - 力扣(LeetCode)的JavaScript 手撕完整版。
一、题目回顾
设计并实现一个 LRU(最近最少使用)缓存 数据结构:
LRUCache(int capacity):初始化容量int get(int key):存在返回值,不存在返回-1void put(int key, int value):插入 / 更新;满了就淘汰最久未使用- 要求:
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]
二、核心原理:为什么必须是「Map + 双向链表」?
要满足 O(1) 必须两个结构配合:
-
哈希表(Map)
- 快速查找:
key → node - 没有它,找节点要 O (n)
- 快速查找:
-
双向链表
- 记录访问顺序
- 头部 = 最近使用
- 尾部 = 最久未使用(要删就删它)
- 支持 O (1) 删除、移动、插入头部
为什么不用单向链表?因为删除节点需要前驱节点,单向链表要遍历找前驱 → O (n)。
三、结构设计
1. Node 节点结构
function Node(key, value) {
this.key = key; // 存key!删除时要用来清map
this.value = value;
this.prev = null; // 刚创建,前后没人
this.next = null;
}
prev/next初始null:表示还没连接到链表- 节点必须存
key:删除尾节点时,才能去map里删掉对应 key
2. LRUCache 构造函数(this 到底是谁)
var LRUCache = function(capacity) {
this.capacity = capacity; // 最大容量
this.map = new Map(); // key → Node(O(1)查找)
// 虚拟头尾节点(占位,永不删除)
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
// 初始链表:head <-> tail
this.head.next = this.tail;
this.tail.prev = this.head;
};
这里的 this = 你 new 出来的缓存实例
const cache = new LRUCache(2);
// cache 就是 this
this.map:每个缓存自己独立的哈希表- 虚拟头尾:避免空链表、边界判断,所有真实节点插中间
四、4 个工具方法(链表操作)
1. 添加到头部(最近使用)
LRUCache.prototype.addToHead = function(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
};
2. 删除任意节点
LRUCache.prototype.removeNode = function(node) {
let prev = node.prev;
let next = node.next;
prev.next = next;
next.prev = prev;
};
3. 移到头部(get / 更新时调用)
LRUCache.prototype.moveToHead = function(node) {
this.removeNode(node);
this.addToHead(node);
};
4. 删除尾部(最久未使用)
LRUCache.prototype.removeTail = function() {
let tailNode = this.tail.prev;
this.removeNode(tailNode);
return tailNode;
};
五、核心 API 实现
1. get 方法(取值)
LRUCache.prototype.get = function(key) {
// 不存在返回-1
if (!this.map.has(key)) return -1;
// 取到节点
let node = this.map.get(key);
// 移到头部 = 标记最近使用
this.moveToHead(node);
return node.value;
};
this.map:当前缓存实例自己的哈希表this:谁调用get,this就是谁
2. put 方法(存值 / 更新)
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
// 已存在:更新 + 移到头部
let node = this.map.get(key);
node.value = value;
this.moveToHead(node);
} else {
// 不存在:新建节点
let newNode = new Node(key, value);
this.map.set(key, newNode);
this.addToHead(newNode);
// 超容量:删最久未使用(尾部)
if (this.map.size > this.capacity) {
let tailNode = this.removeTail();
this.map.delete(tailNode.key); // 同步清map
}
}
};
六、完整可提交代码(LeetCode 直接过)
function Node(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
var LRUCache = function(capacity) {
this.capacity = capacity;
this.map = new Map();
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
};
LRUCache.prototype.addToHead = function(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
};
LRUCache.prototype.removeNode = function(node) {
let prev = node.prev;
let next = node.next;
prev.next = next;
next.prev = prev;
};
LRUCache.prototype.moveToHead = function(node) {
this.removeNode(node);
this.addToHead(node);
};
LRUCache.prototype.removeTail = function() {
let tailNode = this.tail.prev;
this.removeNode(tailNode);
return tailNode;
};
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) return -1;
let node = this.map.get(key);
this.moveToHead(node);
return node.value;
};
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
let node = this.map.get(key);
node.value = value;
this.moveToHead(node);
} else {
let newNode = new Node(key, value);
this.map.set(key, newNode);
this.addToHead(newNode);
if (this.map.size > this.capacity) {
let tailNode = this.removeTail();
this.map.delete(tailNode.key);
}
}
};
七、高频面试问答
- 为什么用 Map? O (1) 查找 key 对应的 Node。
- 为什么用双向链表? O (1) 删除、移动、插入,不需要找前驱。
- Node 里为什么要存 key? 删除尾节点时,要拿到 key 去清 map。
- 虚拟头尾有什么用? 不用判断空链表、边界条件,代码更干净。
- this 到底是谁?
new LRUCache()出来的实例对象。 - 为什么 map 存 node 不存 value? 要操作链表(移动、删除)必须拿到整个节点。
八、总结
LRU 本质就一句话:哈希表保证快查,双向链表保证顺序,两者配合实现 O (1) 缓存淘汰。