leetcode 146/ LCR 031 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 <= 3000
0 <= key <= 10000
0 <= value <= 105
- 最多调用
2 * 105
次get
和put
解题思路
-
get
、put
需要以O(1)
的时间复杂度执行根据此描述,可以想到使用
Map
(或者Object
),通过映射的方式获取、添加关键字。 -
缓存存在最大容量,需要在达到最大容量的时候,将最老的关键字删除
根据此描述,可以想到使用一种有序的数据结构将关键字之间的顺序保存下来。这种数据结构在修改元素顺序、添加新元素、删除元素时的复杂度都是
O(1)
,那么双向链表在这里是最合适的。 -
具体分析类中需要什么方法,各个方法的作用是什么
-
get方法
:根据key
值获取value
-
根据
key
查询Map
,如果元素未保存过,则返回-1; -
根据
key
查询Map
,如果元素保存过,则返回该key
对应的value
,并且需要将此key
对应的链表节点提到最前(类似于将一摞书中抽出一本,放在最顶端;体现在链表中,就是将一个节点移动到链表头部)。
-
-
put方法
:根据key
存放value
- 根据
key
查询Map
,如果元素已保存过,则更新节点的value
,将此key
对应的链表节点提到最前 - 根据
key
查询Map
,如果元素未保存过,则在链表起点处创建新节点。如果超过了最大容量,还要考虑删除尾节点。
- 根据
-
根据分析上述操作,可以封装方法:
#moveToFirst
:将此key
对应的链表节点提到最前#putInList
:在链表起点处创建新节点
-
双向链表操作关键点
-
dummyNode
为了处理数据结构操作中的边界条件,避免了对空数据结构的特殊处理,从而简化了代码的编写和逻辑。通过使用哑节点,开发者可以更加专注于核心逻辑的实现,而不是花费过多时间在处理边界条件上。
-
移动节点(
#moveToFirst
:)
-
插入节点(
#putInList
:)
-
删除节点
代码实现&gpt总结
这段代码实现了一个基于链表和哈希表的最近最少使用(LRU)缓存机制。下面是对该代码的分析:
- 算法逻辑:
LRUCache
利用双向链表和哈希表(Map
)相结合的方式,实现了快速的访问和更新缓存数据的能力。链表用于表示数据的使用顺序,最近使用的节点位于链表头部,而最久未使用的节点位于链表尾部。哈希表则存储键和对应节点的引用,以支持O(1)时间复杂度的访问。当缓存达到其容量上限时,最久未使用的数据会被移除。 - 变量解释:
#capacity
:缓存的最大容量。#dummyNode
:一个哨兵节点(或称虚拟节点),其next
属性指向链表头部,prev
属性指向链表尾部,用以简化边界条件处理。#nodeMap
:哈希表,存储键与节点引用的映射,支持快速访问和检查是否存在某键。Node
类的key
、val
:节点存储的键和值。Node
类的next
、prev
:指向链表中下一个和上一个节点的指针。
- 关键代码分析:
#putInList
方法:在链表头部插入新节点,表示最近使用。#moveToFirst
方法:将一个已存在的节点移动到链表头部,更新其为最近使用状态。get
方法:通过键获取值,如果找到对应的节点,则将其移动到链表头部,表示该数据最近被使用过。put
方法:插入或更新键值对。如果键已存在,更新其值并移动到链表头部;如果键不存在,先检查当前容量,如果未满,则直接在链表头部插入新节点,否则,移除链表尾部节点(最久未使用的数据),然后将新节点插入头部。
- 性能分析:
- 时间复杂度:对于
get
和put
操作,由于哈希表的存在,时间复杂度都是O(1)。 - 空间复杂度:O(N),N为缓存的容量,即哈希表和双向链表存储空间的开销。
- 时间复杂度:对于
- 适用场景:
LRUCache
适用于需要快速访问最近使用的数据的场景,如Web服务器缓存页面,操作系统缓存文件信息等。其目的是保证高效的数据访问,同时在达到预设容量限制时,能自动淘汰最久未使用的数据,保持缓存的有效性和高性能。
class Node {
key: number;
val: number;
next: Node | null;
prev: Node | null;
constructor(key: number, val: number, prev: Node | null, next: Node | null) {
this.key = key;
this.val = val;
this.next = next;
this.prev = prev;
}
}
class LRUCache {
#capacity: number;
#dummyNode: Node;
#nodeMap: Map<number, Node>;
// 将节点插入到首位(#dummyNode之后)
#putInList(key: number, value: number) {
const newNext = new Node(key, value, this.#dummyNode, null);
this.#nodeMap.set(key, newNext);
const oldNext = this.#dummyNode.next as Node;
newNext.next = oldNext;
this.#dummyNode.next = newNext;
oldNext.prev = newNext;
}
// 将节点移动到首位(#dummyNode之后)
#moveToFirst(foundNode: Node) {
const oldFirst = this.#dummyNode.next as Node;
if (oldFirst === foundNode) return; // 已经顺序正确,不需要再移动
oldFirst.prev = foundNode;
this.#dummyNode.next = foundNode;
foundNode.prev!.next = foundNode.next;
foundNode.next!.prev = foundNode.prev;
foundNode.prev = this.#dummyNode;
foundNode.next = oldFirst;
}
constructor(capacity: number) {
this.#capacity = capacity;
this.#nodeMap = new Map();
this.#dummyNode = new Node(-1, -1, null, null);
this.#dummyNode.next = this.#dummyNode;
this.#dummyNode.prev = this.#dummyNode;
}
get(key: number): number {
const foundNode = this.#nodeMap.get(key);
if (!foundNode) return -1;
this.#moveToFirst(foundNode);
return foundNode.val;
}
put(key: number, value: number): void {
const foundNode = this.#nodeMap.get(key);
if (foundNode) {
// 更新节点
foundNode.val = value;
this.#moveToFirst(foundNode);
return;
}
// 插入节点
if (this.#capacity > 0) {
// 未达到最大容量,插入节点
this.#putInList(key, value);
this.#capacity--;
} else {
// 已达到最大容量,插入、删除节点
this.#putInList(key, value);
const toDel = this.#dummyNode.prev as Node;
this.#dummyNode.prev = toDel.prev;
toDel.prev!.next = toDel.next;
this.#nodeMap.delete(toDel.key);
}
}
}
"Knowledge isn't free. You have to pay attention."