[146. LRU 缓存]
「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
题目描述
请你设计并实现一个满足 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
思路
这道算法题要求我们设计一种数据结构来实现 LRU (最近最少使用),且函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。首先,get和put的复杂度为O(1),我们很容易想到基本的数据结构哈希表,在C++语言中,unordered_map的底层实现就是哈希表,是我们需要使用的。之后我们需要实现LRU的要求,具体来说,就是最近访问的优先度最高,最晚使用的优先度最低,就是依据优先度进行排列。很容易让我们我们想起队列一类的东西,但是队列不符合要求。需要我们自己实现一个特殊的队列,这时,就可以用基本的数据结构双向链表了。将这两种数据结构进行结合,就可以实现LRU了。
代码实现
在明确那两种数据结构后,就需要我们来思考需要什么操作了。 首先,put需要我们将一个节点放入队列,或者改变已有节点的值。这里涉及到新建一个节点放在头部并且将超出容量的尾节点删除,修改一个节点后放在头部,两种操作。进一步抽象,后者可以理解为先在队列中删除节点,在重新从头插。这里可以抽象出,删除节点,头插节点两个基本函数。
struct DlinkedNode{
DlinkedNode* pre;
DlinkedNode* next;
int key;
int val;
DlinkedNode():pre(nullptr),next(nullptr),key(0),val(0){};
DlinkedNode(int _key,int _val):pre(nullptr),next(nullptr),key(_key),val(_val){};
}; //自定义的双向链表,其中的数据为键和值,用于搭建优先级队列。
class LRUCache {
private:
unordered_map<int,DlinkedNode*> cache; // 用于记录对应的键和节点的地址
DlinkedNode* dummyH;
DlinkedNode* dummyD; // 虚拟的头尾节点,用来记录顺序,减少讨论
int size; // 当前大小
int capsize; // 设置的大小
void moveToHead(DlinkedNode* node){
DelNode(node);
addToHead(node); // 移动到头节点,相当于删除节点再重新从头插
}
void addToHead(DlinkedNode* node){
node->next=dummyH->next;
node->pre=dummyH; // 先设定插入节点的前后指针,此时对链表无影响
dummyH->next->pre=node;
dummyH->next=node; // 再修改原来的链表 先是前,再是后
}
void DelNode(DlinkedNode* node){
node->next->pre=node->pre;
node->pre->next=node->next; // 直接节点前后节点的指针的指向,从而改变链表。
//此时该节点仍然存在,没有被析构,依然存在哈希表中,只是不在链表里了。
}
DlinkedNode* removeTail(){
DlinkedNode* tmp=dummyD->pre;
DelNode(tmp);
return tmp; // 从尾移除,说明要完全析构
}
public:
LRUCache(int capacity) :capsize(capacity),size(0){
dummyH=new DlinkedNode();
dummyD=new DlinkedNode();
dummyH->next=dummyD;
dummyD->next=dummyH; // 容量,头尾指针初始化
}
int get(int key) {
if(cache.count(key)==0)
return -1; // 当前键值不存在
else{
DlinkedNode* cur = cache[key]; // 从哈希表中找到对应的节点
moveToHead(cache[key]); // 移到头,最高优先级
return cache[key]->val; // 返回值
}
}
void put(int key, int value) {
if(cache.count(key)==0){ // 新的键
DlinkedNode* cur = new DlinkedNode(key,value);
cache[key]=cur; //更新哈希表
addToHead(cur); //头插
size++;
if(size>capsize){ // 超出最大容量,析构尾端的节点
DlinkedNode* ToDel = removeTail();
cache.erase(ToDel->key); //更新哈希表
delete ToDel;
size--;
}
}
else{
DlinkedNode* cur = cache[key]; // 旧的键,更新值
cur->val=value;
moveToHead(cur); // 移到头
}
}
};
/**
* 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);
*/
总结
上述内容对使用基本数据结构数据设计有特殊要求的数据结构进行简单分析,需要对基本数据结构有充分的了解,以及对使用的语言的数据结构底层实现有一定的了解。