一、介绍
1.题目描述
题目链接:
https://leetcode-cn.com/problems/lru-cache/
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache
类:
LRUCache(int capacity)
以正整数作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1)
时间复杂度内完成这两种操作?
2.测试样例
["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","put","put","get","put","put","get"]
[[2],[2,1],[2,2],[2],[1,1],[4,1],[2]]
# [null,null,null,2,null,null,-1]
["LRUCache","put","put","get","put","get","put","get","get","get"]
[[2],[1,0],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
# [null,null,null,0,null,-1,null,-1,3,4]
["LRUCache","put","put","get","put","put","get"]
[[2],[2,1],[2,2],[2],[1,1],[4,1],[2]]
# [null,null,null,2,null,null,-1]
二、题解
1、map+双向链表
链表:链表可以实现对节点的快速插入和删除操作
map:map映射可以将键值对进行匹配
在这道题的问题中,我们需要插入键值对,又需要对数据进行删除,联想到链表+map的组合。其中map键为key,值为链表的一个节点。一个节点将存储键和值,方便数据处理。
由于删除的位置是随机的,因此需要使用双向循环链表。
k | val | *pre | *next | |
---|---|---|---|---|
Node | key | value | 初始为node | 初始为node |
key | value | |
---|---|---|
map | key | Node *p |
思路:
对于最近最少使用算法,若一直插入元素,最早被插入的元素应最先被删除。
由这一特性,可以想到若通过put更新/插入元素,或通过get获取元素,则表示该元素最近被使用过,为保证顺序,应将其原位置删除,重新插到末尾。
这种删除插入操作用双向链表实现,由map找到对应节点位置,删除该节点,重新插入末尾。
当链表满后,头结点即为最近最少使用,直接删除,然后在末尾插入新节点。
当操作为LRUCache:
- 初始化链表头结点node,pre和next都指向自己,构成双向循环
当操作为put:
- 若不存在该键
- 若存储未满,插入到node前【相当于链表末尾】
- 若存储已满,删除node后的节点【即链表头】,将新节点插入到node前【相当于链表末尾】
- 创建map映射
- 若存在该键
- 链表中删除该节点,在最后重新插入该节点,map更新映射
当操作为get:
- 若不存在,返回-1
- 若存在,将该节点删除,在末尾重新插入【保证最近最少使用的删除顺序】
注意事项:
- 由于各个环节都会用到插入操作,可以将插入封装成一个函数
- put中每个可能性最后都要在链表末尾插入元素,并更新map,可以统一运行
class LRUCache {
int n,num=0;
struct Node{
int val,k;
Node *next,*pre;
};
Node* node = new Node;
map<int,Node*> mp;
public:
LRUCache(int capacity) { // 初始化循环链表
n=capacity;
node->next=node;
node->pre=node;
}
int get(int key) {
if(mp.count(key)){ // 如果存在,更新并返回
Node *p=mp[key]; // 找到并删除该节点
p->pre->next=p->next;
p->next->pre=p->pre;
inser(node,p); // 重新在末尾插入该节点
return p->val;
}
else return -1;
}
void put(int key, int value) {
Node *p=new Node();
p->val=value;
p->k=key;
if(mp.count(key)){ // 如果已存在,更新
p=mp[key]; // 找到并删除该节点
p->pre->next=p->next;
p->next->pre=p->pre;
p->val=value;
}
else if(num>=n){ // 如果已满,删除第一个节点,在最后插入节点
mp.erase(node->next->k); // map删除第一个节点
node->next=node->next->next; // node删除第一个节点
node->next->pre=node;
}
else num++;
inser(node,p); // 在node前面插入新节点
mp[key]=p; // 更新map
}
void inser(Node* node, Node *p){ // 在node前面插入新节点
node->pre->next=p;
p->pre=node->pre;
p->next=node;
node->pre=p;
}
};
2、map+双端队列+栈🟢(超时)
还有一种时间复杂度为O(n)的方法,但是超时了。
其使用到的数据结构:map+双端队列+栈,其中栈和队列存储键即可,用于调整顺序
当操作为LRUCache:
- 初始化容量
当操作为put:
- 若不存在该键
- 若存储未满,插入到队列末尾
- 若存储已满,删除队列头,新键插入队列尾
- 更新map
- 若存在该键【需要将该键重新插入到末尾,由于队列不能直接去中间的元素,所以借助栈】
- 从前向后弹出元素并入栈,直到遇到该键
- 将该键弹出,在末尾重新插入
- 将栈中元素弹出并在队列头插入
- 更新map
当操作为get:
- 若不存在,返回-1
- 若存在,将该节点删除,在末尾重新插入【保证最近最少使用的删除顺序】
class LRUCache {
int n;
public:
LRUCache(int capacity) {
n=capacity;
}
int get(int key) {
if(mp.count(key)){ // 如果存在,更新并返回
while(q.front()!=key){
stk.push(q.front());
q.pop_front();
}
q.pop_front();
q.push_back(key);
while(!stk.empty()){
q.push_front(stk.top());
stk.pop();
}
return mp[key];
}
else return -1;
}
void put(int key, int value) {
if(mp.count(key)){ // 如果已存在,更新
mp[key]=value;
while(q.front()!=key){
stk.push(q.front());
q.pop_front();
}
q.pop_front();
q.push_back(key);
while(!stk.empty()){
q.push_front(stk.top());
stk.pop();
}
}
else if(mp.size()<n){ // 如果未满
mp[key]=value;
q.push_back(key);
}
else{ // 如果已满
mp.erase(q.front());
mp[key]=value;
q.pop_front();
q.push_back(key);
}
}
};