题目链接: 面试题 16.25. LRU 缓存
📙题目描述:

✏️题目分析:
我们通过阅读题目可知题目中出现键值对,我们遇到键值对就要想到哈希表。题目中让我们构建一个"最近最少使用",并且支持插入和删除数据,对于有出入顺序的问题,我们就要想到栈,队列或者是链表。这道题我们可以用双向链表。
通过示例我们可以发现这道题在访问数据的时候是随机的,由于哈希表支持能够在时间复杂度为O(1)内查找元素,如果哈希表中的值包含链表的结点信息,就能实现在O(1)内定位到链表中的某个结点。所以我们将来可以定义哈希表的结构为:
cpp
unordered_map<键值,链表的结点>
这样就能实现通过键值来定位到链表中的结点。
由于哈希表只负责定位查找功能,所以我们的双向链表结点中就应该包含key和value,所以双向链表的结点我们可以定义成:
cpp
struct DLinkedNode
{
int key,value;
DLinkedNode* prev;//双向链表中的上一个结点
DLinkedNode* next;//双向链表中的下一个结点
};
🌵对于get操作:
- 如果密钥
key不存在
返回-1 - 如果密钥
key存在,则获取密钥的值。并且当前密钥对应的结点为最近最新使用的结点。我们把最近最少使用的结点放在链表的尾部,最近最新使用的结点放在链表的头部。
🌿如何把最近最新使用的结点放在双向链表的头部呢?
我们可以先将此结点移出双向链表,再将此结点添加到链表的头部,所以这里就需要实现两种方法removeNode和addToHead
removeNode
cpp
void removeNode(DLinkedNode* node)
{
node->prev->next->node->next;//node结点的上一个结点的next指向node结点的下一个结点
node->next->prev->node->prev;//node结点的下一个结点的prev指向node结点的上一个结点
}

addToHead
cpp
void addToHead(DLinkedNode* node)
{
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
🌵对于put操作:
-
如果密钥
key不存在用
key和value创建一个新的结点,并用key将此结点添加到哈希表中,调用addToHead将此结点放到双向链表的头部,判断结点的数量是否超出了容量,如果超出容量,删除掉最近最少使用的那个结点,即尾部结点,并删除哈希表中对应的项。 -
如果密钥
key存在与
get操作类似,先通过key使用哈希表找到此结点在双向链表中的位置,更新value,用moveToHead将此结点移动到链表的头部如果链表的数量大于链表的容量,我们就需要删除最近最少使用的结点。双向链表的尾部结点即为最近最少使用的,我们要删除这个结点就要先找到这个结点。我们使用
removeTail()找到这个结点并删除,并且删除哈希表中对应的项,delete掉这个结点防止内存泄漏。
moveToHeaad
cpp
void moveToHead(DLinkedNode* node)
{
removeNode(node);
addToHead(node);
}
removeTail
cpp
DLinkedNode* removeTail()
{
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
🔖细节问题:我们可以定义一个伪头节点和一个伪尾结点,这样我们在增加或者删除结点的时候就不需要判断相邻的结点是否存在。
🐾 代码实现:
cpp
//定义双向链表结点
struct DLinkedNode
{
int key, value;
DLinkedNode* prev; //双向链表结点的上一个结点
DLinkedNode* next;//双向链表结点的下一个结点
DLinkedNode(int _key = 0, int _value = 0)
:key(_key)
,value(_value)
,prev(nullptr)
,next(nullptr)
{
}
};
class LRUCache
{
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))
{
//说明key不存在
return -1;
}
else
{
//key存在
DLinkedNode* node = cache[key];//通过哈希表定位到key所对应的结点
//将key对应的结点移动到双向链表的头部
moveToHead(node);
return node->value;//最后返回该结点的值
}
}
void put(int key, int value)
{
//首先判断key是否存在
if (!cache.count(key))
{
//key不存在
DLinkedNode* node = new DLinkedNode(key, value);//使用key和value创建一个新的结点
addToHead(node);//将新结点添加到双向链表的头部
cache[key] = node;//将新结点添加到哈希表中
++size;
if (size > capacity)
{
//如果节点数超出双向链表的容量
DLinkedNode* removed = removeTail();//删除尾部的结点
cache.erase(removed->key);//删除哈希表中对应的项
delete removed;//防止内存泄漏
--size;
}
}
else
{
//key存在
DLinkedNode* node = cache[key];//通过哈希表定位key对应的node结点
node->value = value;//修改node对应的value值
moveToHead(node);//将该节点移动到双向链表的头部
}
}
void moveToHead(DLinkedNode* node)
{
removeNode(node);
addToHead(node);
}
void removeNode(DLinkedNode* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
void addToHead(DLinkedNode* node)
{
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
DLinkedNode* removeTail()
{
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
private:
unordered_map<int, DLinkedNode*> cache;//通过哈希表实现O(1)的链表结点查找
DLinkedNode* head;//伪头节点
DLinkedNode* tail;//伪尾结点
//有伪节点的存在,就不需要查找相邻结点是否存在
int size; //有效元素个数
int capacity;//空间大小
};
写完发现的问题:如果新添加的结点为0,0,和伪结点一样呢?
不影响,因为哈希表中并没有把伪结点的键值key添加到哈希表中。