题目描述
实现一个 LRU (Least Recently Used,最近最少使用) 缓存机制。该缓存应该支持以下操作: 获取数据 get 和写入数据 put。
获取数据 get(key):如果缓存中存在 key,则获取其对应的 value 值;否则返回 -1。
写入数据 put(key, value):如果 key 不存在,则写入其对应的 value 值;如果 key 已经存在,则更新其对应的 value 值,并把它移到最前面。
缓存容量固定且不会超过指定大小,一旦缓存已满,则需要移除最近最少使用的数据,腾出位置给新的数据。
解题思路
由于本题需求中包含了缓存淘汰策略、缓存数据结构的维护及查找,因此我们可以采用 Map + 双向链表(DoublyLinked List) 来实现本题。
- 使用 Map 存储键值对,以保证在 O(1) 时间内查询元素是否存在
- 使用 DoublyLinked List 存储元素节点,以便实现快速删除及添加元素,并保证元素访问顺序
- 当元素数量达到缓存容量时,应该从链表尾部开始淘汰元素
代码实现
class Node {
public key: number;
public value: number;
public prev: Node;
public next: Node;
constructor(key:number, value: number, prev?: Node, next?: Node) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
class LRUCache {
private size: number; // 缓存容量,用于检查是否已经超过容量
private hashMap: {[key: number]: Node}; // 哈希表,用于快速查找节点
private left: Node; // 链表头
private right: Node; // 链表尾
constructor(size: number) { // 初始化方法
this.size = size;
this.hashMap = {};
// 创建哑节点(dummy nodes)
this.left = new Node(0,0);//left.next为Least Recently Used (LRU)
this.right = new Node(0,0);//right.prev为Most Recently Used
this.left.next = this.right; // 头节点指向尾节点
this.right.prev = this.left; // 尾节点指向头节点
}
// 从双向链表删除一个节点
remove(node: Node): void{
let prev = node.prev;
let next = node.next;
prev.next = next;
next.prev = prev;
}
// 在双向链表头部插入节点(该节点将成为最近使用的节点)
insert(node: Node): void{
let next = this.left.next; // 旧头结点
// 新节点插入到“left dummy”和“next”之间
this.left.next = node;
next.prev = node;
// 设置新节点的指针
node.next = next;
node.prev = this.left;
}
get(key: number): number { // 查询缓存
if (key in this.hashMap) { // 如果哈希表中存在该键值对
this.remove(this.hashMap[key]);// 将节点移除
this.insert(this.hashMap[key]);// 插入到头部,作为最近使用的节点
return this.hashMap[key].value; // 返回查找到的值
}
return -1; // 没有查找到,返回-1
}
put(key: number, valueue: number): void { // 添加缓存
if (key in this.hashMap) { // 如果哈希表中已经存在该键值对
this.remove(this.hashMap[key]); // 先将旧节点移除
}
let newNode = new Node(key, valueue); // 创建新节点
this.hashMap[key] = newNode; // 在哈希表中添加该键值对
this.insert(newNode); // 在双向链表头部插入新节点
if(Object.keys(this.hashMap).length > this.size){
// 检查是否超出缓存容量
let lru = this.right.prev; // 最近最少使用(LRU)的节点为尾节点的上一个节点
this.remove(lru); // 将该节点从双向链表删除
delete this.hashMap[lru.key]; // 从哈希表中删除键值对
}
}
}
时间复杂度
put 和 get 操作的时间复杂度均为 O(1)。其中,put 操作中需要在 Map 中查询 key 是否存在,也是 O(1) 的时间复杂度。
空间复杂度
空间复杂度为 O(capacity),需要存储容量大小个元素。