一、数据结构设计
为了实现 get 和 put 操作的时间复杂度 O(1) ,我们组合了两种数据结构:
-
哈希表(
Map或普通对象)- 存储键到双向链表节点的映射。
- 作用:通过
key直接定位到节点,实现 O(1) 的查找。
-
双向链表(
Node类实现)- 维护所有节点的 使用顺序:链表头部(head 之后)是最近使用的节点,链表尾部(tail 之前)是最久未使用的节点。
- 作用:在 O(1) 时间内完成节点的 移动到头部、删除尾部 等操作。
此外,使用两个 哨兵节点(伪头 head 和伪尾 tail),避免处理链表为空或只有一个节点时的边界条件,使插入和删除操作更简洁。
以下是 JavaScript 手写的 LRU 缓存类,使用哈希表 + 双向链表实现,确保 get 和 put 操作时间复杂度为 O(1):
javascript
class LRUCache {
/**
* @param {number} capacity 缓存容量
*/
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // 键 → 节点
// 创建哨兵头尾节点,简化边界处理
this.head = new Node(0, 0);
this.tail = new Node(0, 0);
this.head.next = this.tail;
this.tail.prev = this.head;
}
/**
* 获取键对应的值,并将该节点移动到链表头部(最近使用)
* @param {number} key
* @return {number}
*/
get(key) {
if (!this.cache.has(key)) {
return -1;
}
const node = this.cache.get(key);
this._moveToHead(node);
return node.value;
}
/**
* 插入或更新键值对,并将节点置于头部(最近使用)
* 若容量超限,删除尾部节点(最久未使用)
* @param {number} key
* @param {number} value
* @return {void}
*/
put(key, value) {
if (this.cache.has(key)) {
// 已存在:更新值并移到头部
const node = this.cache.get(key);
node.value = value;
this._moveToHead(node);
} else {
// 不存在:新建节点
if (this.cache.size === this.capacity) {
// 容量已满,删除尾部节点(最久未使用)
const tailNode = this.tail.prev;
this._removeNode(tailNode);
this.cache.delete(tailNode.key);
}
const newNode = new Node(key, value);
this.cache.set(key, newNode);
this._addToHead(newNode);
}
}
/**
* 将节点从原位置移除,并添加到头部
* @param {Node} node
*/
_moveToHead(node) {
this._removeNode(node);
this._addToHead(node);
}
/**
* 从链表中移除节点
* @param {Node} node
*/
_removeNode(node) {
const prev = node.prev;
const next = node.next;
prev.next = next;
next.prev = prev;
}
/**
* 将节点插入到哨兵头节点之后(头部)
* @param {Node} node
*/
_addToHead(node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}
}
/**
* 双向链表节点
*/
class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
使用示例
javascript
const lru = new LRUCache(2);
lru.put(1, 1); // 缓存: {1=1}
lru.put(2, 2); // 缓存: {1=1, 2=2}
console.log(lru.get(1)); // 返回 1,并移动 1 到头部 → 缓存顺序: 2,1
lru.put(3, 3); // 容量已满,删除尾部 2 → 缓存: {1=1, 3=3}
console.log(lru.get(2)); // 返回 -1 (未找到)
lru.put(4, 4); // 容量已满,删除尾部 1 → 缓存: {3=3, 4=4}
console.log(lru.get(1)); // 返回 -1
console.log(lru.get(3)); // 返回 3
console.log(lru.get(4)); // 返回 4
复杂度说明
- get: 哈希表查找 O(1) + 链表移动 O(1) → 总体 O(1)
- put: 哈希表插入/更新 O(1) + 可能删除尾部 O(1) + 链表操作 O(1) → 总体 O(1)