LeetCode146 LRU缓存机制
https://leetcode-cn.com/problems/lru-cache/
题目:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
1.LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
2.int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
3.void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
示例:
输入
[“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
图解
背景
内存是有限的,在内存不够用时,必须要淘汰旧页,通常来说就是采用LRU淘汰掉最久未使用的页。LRU简单来说就是,最频繁使用的页在LRU列表的队头,而最少使用的页在LRU列表的队尾,当缓冲池不能存放新读取到的页时,将首先释放LRU列表的队尾的页。
分析
LRUCache主要要实现两个功能:
int get(int key)
如果关键字存在于LRU列表中,则返回关键字的值,并把该关键字放在列表的队头,否则返回 -1 。void put(int key, int value)
如果关键字已经存在,则变更其数据值,并把该关键字放在列表的队头;如果关键字不存在,则插入该组「关键字-值」,并把该关键字放在列表的队头。当LRU列表容量达到上限时,它应该删除最久未使用的数据值,即队尾的数据值,从而为新的数据值留出空间。
用什么方式实现这个LRU列表呢?由于上述功能涉及到了一个关键操作:调至队头 。而这个操作涉及到了两方面内容:删除和插入 。同样还有一个关键操作:删除最久未使用的数据值,即队尾的数据值 。可以看到,涉及的操作都是插入和删除,而我们知道链表进行插入和删除操作仅需修改指针,时间复杂度为O(1),所以我们敲定:使用链表表示这个LRU列表。
那么使用单链表还是双链表?答案是使用双链表。为什么要用双链表?只要是由于链表的删除操作需要找到当前节点的前一个节点,双链表可以在O(1)内找到前一个节点从而在O(1)内删除当前节点。似乎单向链表也可以快速删除,为什么在这里为什么不能使用呢?
1.不是真正的删除,而是替换。真正删除的是下一个节点。
2.无法删除尾节点。
上述功能还涉及到了一个关键操作:判断关键字是否存在 。那么如何判断关键字是否存在呢? 或者说如何试图去找到这个关键字呢? 我们知道顺序表具有随机访问特性,可以“忽然”访问到某个值,这样就可以在O(1)内试图去找到这个关键字。而链表没有这种随机访问特性,我们当然希望链表具有这种特性,否则无法在O(1)内完成上述功能。那么我们可以使用哈希表来改造双链表,借助哈希表使得双链表具有了随机访问特性。具体来说就是 哈希表存储 key,而哈希表的 value 指向双向链表实现的 LRU 的 Node 节点,节点的数据域是key和value,如图所示。
这样一来,我们就可以我们首先使用哈希表进行定位,找出缓存项在双链表中的位置,随后执行相应的插入和删除操作,即可在 O(1)内完成 get 或者 put 操作。具体来说:
1.对于 get 操作,首先判断 key 是否存在:
1)如果 key 不存在,则返回 -1;
2)如果 key 存在,通过哈希表定位到该节点,并将其插入到头节点,最后返回该节点的值。
2.对于 put 操作,首先判断 key 是否存在:
1)如果 key 不存在,使用 key 和 value 创建一个新的节点,并将其插入到头节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双链表的尾节点,并删除哈希表中对应的项;
2)如果 key 存在,通过哈希表定位到该节点,将该节点的value值改为指定的value值,并将其插入到头节点。
细节
- 双链表的插入操作
-
双链表的删除操作
-
使用伪头节点和伪尾节点统一头尾节点和一般节点的操作
使得在头尾节点处插入节点和删除节点的时候就不需要检查相邻的节点是否存在。
代码
//双链表
struct DLinkList
{
int key;
int value;
DLinkList *pre;
DLinkList *next;
DLinkList():key(0), value(0), pre(nullptr), next(nullptr){}
DLinkList(int key, int value):key(key), value(value),
pre(nullptr), next(nullptr){}
};
class LRUCache
{
private:
unordered_map<int, DLinkList*> cache;
DLinkList *head;
DLinkList *tail;
int size;
int capacity;
public:
LRUCache(int capacity)
{
//使用伪头结点和伪尾结点
head = new DLinkList();
tail = new DLinkList();
head->next = tail;
tail->pre = head;
size = 0;
this->capacity = capacity;
}
//删除节点
void removeNode(DLinkList* node)
{
node->pre->next = node->next;
node->next->pre = node->pre;
}
//将节点插入到头节点
void addToHead(DLinkList* node)
{
node->next = head->next;
head->next->pre = node;
node->pre = head;
head->next = node;
}
//删除尾结点
DLinkList* removeTail()
{
DLinkList* node = tail->pre;
removeNode(node);
return node;
}
int get(int key)
{
//判断key是否存在
if (cache.count(key) == 0)
{
return -1;
}
else
{
DLinkList* node = cache[key];
removeNode(node);
addToHead(node);
return node->value;
}
}
void put(int key, int value)
{
//判断key是否存在
if (cache.count(key) == 0)
{
DLinkList* node = new DLinkList(key, value);
cache[key] = node;
addToHead(node);
++size;
//判断缓存容量是否达到上限
if (size > capacity)
{
DLinkList* removed = removeTail();
cache.erase(removed->key);
delete removed;
--size;
}
}
else
{
DLinkList* node = cache[key];
node->value = value;
removeNode(node);
addToHead(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);
*/