LRU (Least Recently Used) 的核心在于:"如果一个数据最近被访问过,那么它在未来被访问的概率也更高。"
为了达到 的操作效率,我们需要两种数据结构的结合:
- 哈希表 (Map) :提供 的随机访问能力,通过
key快速定位节点。 - 双向链表 (Doubly Linked List):提供 的节点移动能力,用来维护访问顺序。
实现细节优化
1. 定义双向链表节点
使用双向链表是因为删除一个节点时,我们需要知道它的前驱节点。
javascript
class ListNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
2. LRU 缓存类定义
技巧: 使用"哨兵节点"(Dummy Head/Tail)可以极大简化边界条件的处理,避免判断 null。
javascript
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
// 初始化哨兵节点,它们不存储实际数据
this.head = new ListNode(-1, -1);
this.tail = new ListNode(-1, -1);
this.head.next = this.tail;
this.tail.prev = this.head;
}
关键内部方法(封装逻辑)
将复杂的链表操作封装为私有方法,使 get 和 put 的主逻辑像英语一样易读。
javascript
// 将节点移至头部(表示最近使用)
moveToHead(node) {
this.removeNode(node);
this.addToHead(node);
}
// 从链表中彻底断开节点
removeNode(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 在哨兵头节点后插入新节点
addToHead(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}
// 弹出最久未使用的节点(尾部哨兵前的一个)
removeTail() {
const lastNode = this.tail.prev;
this.removeNode(lastNode);
return lastNode; // 返回节点以便从 map 中删除
}
主接口实现
javascript
/** * @param {number} key
* @return {number}
*/
get(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
this.moveToHead(node); // 刷新位置
return node.value;
}
/** * @param {number} key
* @param {number} value
*/
put(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key);
node.value = value; // 更新值
this.moveToHead(node);
} else {
const newNode = new ListNode(key, value);
this.map.set(key, newNode);
this.addToHead(newNode);
if (this.map.size > this.capacity) {
const removed = this.removeTail();
this.map.delete(removed.key); // 必须在节点中存储 key 才能在此删除 map 记录
}
}
}
}