目录
- 引言
- [LRU 缓存](#LRU 缓存)
-
- 官方解题
- LRU实现
- [📌 实现步骤分解](#📌 实现步骤分解)
-
- [步骤 1:定义双向链表节点](#步骤 1:定义双向链表节点)
- [步骤 2:创建伪头尾节点(关键设计)](#步骤 2:创建伪头尾节点(关键设计))
- [步骤 3:实现链表基础操作](#步骤 3:实现链表基础操作)
-
- [操作 1:添加节点到头部](#操作 1:添加节点到头部)
- [操作 2:移除任意节点](#操作 2:移除任意节点)
- [步骤 4:实现关键组合操作](#步骤 4:实现关键组合操作)
-
- [操作 3:移动节点到头部(访问时调用)](#操作 3:移动节点到头部(访问时调用))
- [操作 4:移除尾部节点(淘汰时调用)](#操作 4:移除尾部节点(淘汰时调用))
- [步骤 5:初始化缓存结构](#步骤 5:初始化缓存结构)
- [步骤 6:实现 get 操作](#步骤 6:实现 get 操作)
- [步骤 7:实现 put 操作](#步骤 7:实现 put 操作)
- [🔑 关键设计验证点](#🔑 关键设计验证点)
- [🚀 完整实现代码](#🚀 完整实现代码)
- [💡 实现要点总结](#💡 实现要点总结)

引言
这题好像几年前就是hard。后面变成medium了。感觉就是普通人只做1~2遍,都不能独立记住整个实现过程。做到第3遍时大概能记得思路开始独立写代码了,但是会遇到各种问题不能bug free的AC掉。需要练很多遍才能真的在面试中写对的。这题应该就是靠代码功底的,看能不能现场写出bug free或者能debug出来。
上面的这个是别人写的评论,看着确实是这么回事。今天能把这道题写完就算ok了。这个相当于设计一个类了。
LRU 缓存
- 🎈 题目链接:
- 🎈 做题状态:
官方解题
这道题涉及的知识面确实比较多,第一次做的话不容易ac。可以多写几次。
cpp
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity) : capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
// 判断当前key是否存在
if (!cache.count(key))
{
return -1;
}
// 通过哈希表快速的找到节点
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
// 判断当前key是否存在
if (!cache.count(key))
{
// 不存在,则创建一个新节点
DLinkedNode* node = new DLinkedNode(key, value);
// 将其添加到哈希表中
cache[key] = node;
// 将新的节点添加到双向链表的头部,因为是新使用的
addToHead(node);
++size;
// 判断当前链表大小是否超过容量,如果超出容量,则需要删除尾部节点
if (size > capacity)
{
DLinkedNode* removed = removedTail();
cache.erase(removed->key);
delete removed;
--size;
}
}
else
{
// 如果当前key存在,先通过哈希表定位,再修改value,并移动到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
// 将节点添加到头部
void addToHead(DLinkedNode* node)
{
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
// 移除节点(不释放节点内存)
void removeNode(DLinkedNode* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
// 将节点移动到头部
void moveToHead(DLinkedNode* node)
{
removeNode(node);
addToHead(node);
}
// 将移除的节点返回给外部进行处理释放
DLinkedNode* removedTail()
{
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
LRU实现
我将把 LRU 缓存的实现拆解为 7 个清晰的步骤,并配以关键代码片段。让我们像搭积木一样逐步构建这个数据结构:
📌 实现步骤分解
步骤 1:定义双向链表节点
目的:创建存储数据的基本单元
cpp
struct Node {
int key; // 用于删除尾节点时反向查找哈希表
int value;
Node* prev;
Node* next;
Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
步骤 2:创建伪头尾节点(关键设计)
目的:消除边界条件判断,简化链表操作
cpp
class LRUCache {
private:
Node* dummyHead; // 永远指向虚拟头部节点
Node* dummyTail; // 永远指向虚拟尾部节点
// 初始化伪头尾
dummyHead = new Node(-1, -1);
dummyTail = new Node(-1, -1);
dummyHead->next = dummyTail;
dummyTail->prev = dummyHead;
步骤 3:实现链表基础操作
操作 1:添加节点到头部
cpp
void addToHead(Node* node) {
// 新节点的前后指针
node->prev = dummyHead;
node->next = dummyHead->next;
// 调整原有链接
dummyHead->next->prev = node;
dummyHead->next = node;
}
操作 2:移除任意节点
cpp
void removeNode(Node* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
步骤 4:实现关键组合操作
操作 3:移动节点到头部(访问时调用)
cpp
void moveToHead(Node* node) {
removeNode(node); // 从当前位置移除
addToHead(node); // 添加到头部
}
操作 4:移除尾部节点(淘汰时调用)
cpp
Node* removeTail() {
Node* node = dummyTail->prev; // 真实尾节点
removeNode(node);
return node;
}
步骤 5:初始化缓存结构
cpp
class LRUCache {
private:
unordered_map<int, Node*> cache; // 哈希表
int capacity; // 容量限制
public:
LRUCache(int cap) : capacity(cap) {
// 初始化伪头尾(见步骤2)
}
};
步骤 6:实现 get 操作
cpp
int get(int key) {
if (!cache.count(key)) return -1; // 未命中
Node* node = cache[key];
moveToHead(node); // 更新为最近使用
return node->value;
}
步骤 7:实现 put 操作
cpp
void put(int key, int value) {
if (cache.count(key)) { // 已存在
Node* node = cache[key];
node->value = value; // 更新值
moveToHead(node); // 设为最新
} else { // 新增数据
Node* node = new Node(key, value);
cache[key] = node; // 记录到哈希表
addToHead(node); // 添加到链表
if (cache.size() > capacity) {
Node* removed = removeTail(); // 淘汰最旧
cache.erase(removed->key); // 从哈希表删除
delete removed; // 释放内存
}
}
}
🔑 关键设计验证点
测试场景 | 预期操作 | 验证方法 |
---|---|---|
缓存未满时插入 | 新节点添加到头部 | 检查链表头部是否为最新节点 |
缓存已满时插入 | 淘汰尾节点后添加新节点到头部 | 检查链表长度是否保持 capacity |
访问已有节点 | 节点被移动到头部 | 执行get后检查节点位置 |
更新已有节点的值 | 值被更新且节点移到头部 | 检查值变化和节点位置 |
🚀 完整实现代码
cpp
#include <unordered_map>
using namespace std;
struct Node {
int key, value;
Node *prev, *next;
Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, Node*> cache;
Node *dummyHead, *dummyTail;
int capacity;
void addToHead(Node* node) {
node->prev = dummyHead;
node->next = dummyHead->next;
dummyHead->next->prev = node;
dummyHead->next = node;
}
void removeNode(Node* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(Node* node) {
removeNode(node);
addToHead(node);
}
Node* removeTail() {
Node* node = dummyTail->prev;
removeNode(node);
return node;
}
public:
LRUCache(int cap) : capacity(cap) {
dummyHead = new Node(-1, -1);
dummyTail = new Node(-1, -1);
dummyHead->next = dummyTail;
dummyTail->prev = dummyHead;
}
int get(int key) {
if (!cache.count(key)) return -1;
Node* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (cache.count(key)) {
Node* node = cache[key];
node->value = value;
moveToHead(node);
} else {
Node* node = new Node(key, value);
cache[key] = node;
addToHead(node);
if (cache.size() > capacity) {
Node* removed = removeTail();
cache.erase(removed->key);
delete removed;
}
}
}
};
💡 实现要点总结
-
双数据结构协作:
- 哈希表:O(1) 时间查找
- 双向链表:维护访问顺序
-
伪节点的妙用:
- 消除头尾节点的特殊判断
- 统一所有节点的操作逻辑
-
操作原子化:
- 将链表操作分解为独立方法
- 提高代码可读性和可维护性
-
内存管理:
- 淘汰节点时需手动释放内存
- 插入新节点时动态分配内存
通过这种分步实现方式,可以更清晰地理解每个组件的作用,也便于在开发过程中逐步测试验证每个功能的正确性。