LeetCode 面试经典 150_链表_LRU 缓存(66_146_C++_中等)
题目描述:
请你设计并实现一个满足 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
题解:
解题思路:
思路一(哈希表 + 双向链表):
1、通过题目分析,查找操作可采用哈希表增加查找速度,更新使用时间操作可以使用双向链表(一端存储最近使用的结点,另一端存储最久未使用结点)。
put操作:如果key不在缓存中那我们需要进行结点的插入操作,若插入时缓存已满则先删除最久未访问的结点再插入。这里我们可以想到双向链表,尾部存储最近访问结点,头部存储最久未访问结点(也可以头部存储最近访问结点,尾部存储最久未访问结点)。get操作,若存在key则返回结点的value,这里get相当于一个查找,所以我们可以想到哈希表,这样我们就能快速的进行结点的查找和定位。
2、具体思路如下:
① 我们创建一个头结点和一个额外的尾结点来方便结点的插入和删除操作(插入结点在两节点中间 )。
② 插入操作(put):我们将刚插入的元素或者最近使用的元素放在链表的尾部,则头部为最长时间未使用的元素。
若要插入结点key时,之前存在序号为key的结点则移到链表尾部。
若要插入节点key时,之前不存在序号为key的结点,且结点数未满则插入链表尾部,若结点数已满则插入后删除头部结点(并更新哈希表)。
③ 获取操作(get):分析到获取我们很快能想到哈希表,因哈希表能让我们快速的进行查找操作,所以上述插入和删除时需要维护一个哈希表。
若结点不在哈希表中则返回-1。
若存在哈希表中则返回值,并将结点移动到头部。
力扣官方题解链接(有缓存 get() 和put () 过程图很不错)
3、复杂度分析
① 时间复杂度:对于 put 和 get 都是 O(1)。
② 空间复杂度:O(capacity),因为哈希表和双向链表最多存储 capacity+1 个元素。
代码实现(思路一(哈希表 + 双向链表)):
cpp
// 双向链表节点定义
struct DLinkNode {
int key, value; // 键值对
DLinkNode *prev, *next; // 前后指针,指向双向链表中的前一个节点和后一个节点
// 默认构造函数
DLinkNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
// 带参数的构造函数
DLinkNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkNode *> cache; // 哈希表,映射键到节点,提供 O(1) 查找
DLinkNode *head, *tail; // 双向链表的头尾指针
int size; // 当前缓存中元素的数量
int capacity; // 缓存的最大容量
public:
// 构造函数,初始化 LRU 缓存
LRUCache(int _capacity) : capacity(_capacity), size(0) {
// 创建一个虚拟的头尾节点,简化操作
head = new DLinkNode();
tail = new DLinkNode();
// 头节点的下一个节点是尾节点,尾节点的前一个节点是头节点
head->next = tail;
tail->prev = head;
}
// 获取缓存中的元素,若存在则返回值并将其移到链表尾部,表示最近使用
int get(int key) {
if (cache.count(key)) { // 如果缓存中存在该键
// 删除该节点并插入到尾部(表示最近使用)
deleteNode(cache[key]);
insertTail(cache[key]);
return cache[key]->value; // 返回对应的值
}
return -1; // 如果缓存中没有该键,返回 -1
}
// 插入一个新的元素或更新已存在元素的值
void put(int key, int value) {
if (cache.count(key)) { // 如果缓存中已经有该键
cache[key]->value = value; // 更新值
// 删除旧节点并将更新后的节点插入到尾部
deleteNode(cache[key]);
insertTail(cache[key]);
} else {
// 如果缓存中没有该键,创建新的节点
DLinkNode *newNode = new DLinkNode(key, value);
cache[key] = newNode; // 将新节点加入哈希表
insertTail(newNode); // 将新节点插入链表尾部
size++; // 增加缓存大小
// 如果缓存超出容量,移除最久未使用的元素(链表头部的节点)
if (size > capacity) {
cache.erase(head->next->key); // 从哈希表中删除最久未使用的元素
deleteNode(head->next); // 从链表中删除该节点
size--; // 减小缓存大小
}
}
}
// 将节点插入到链表的尾部,表示最近使用
void insertTail(DLinkNode *Node) {
Node->next = tail; // 节点的后继是尾节点
Node->prev = tail->prev; // 节点的前驱是原尾节点的前驱
tail->prev->next = Node; // 原尾节点的前驱的后继指向新节点
tail->prev = Node; // 尾节点的前驱指向新节点
}
// 删除链表中的节点
void deleteNode(DLinkNode *Node) {
Node->next->prev = Node->prev; // 删除节点时,修改其后继节点的前驱指针
Node->prev->next = Node->next; // 删除节点时,修改其前驱节点的后继指针
}
};
部分代码解读
构造函数声明+初始化列表进行变量初始化和赋值
cpp
//下方代码的用法和第一行相同,是构造函数初始化列表,对变量初始化和赋值
DLinkedNode(int _key,int _value):key(_key),value(_value),prev(nullptr),next(nullptr){}
//构造函数
//初始化 capacity 成员变量 为传递给构造函数的参数 _capacity
//初始化 size 成员变量 为 0,表示缓存初始化时为空。
LRUCache(int _capacity):capacity(_capacity),size(0){}
LeetCode 面试经典 150_链表_LRU 缓存(66_146)原题链接
欢迎大家和我沟通交流(✿◠‿◠)