题目
请你设计并实现一个满足 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 <= 10^5
最多调用 2 * 10^5 次 get 和 put
思路
这道题很繁琐,也很综合,基于操作系统关于内存的知识,包含类、结构体、构造函数、链表等内容,非常考察综合能力。其实这是字节跳动的一道二面题,这也提醒我们刷算法题不一定只有那种考虑某个功能的函数,还有这种综合性的难题。
来把一些细节思考清楚:
1.get和put必须要O(1)的时间复杂度,就是给到key就能找到value,这样的索引方式应该想到用map这种基于key-value的映射结构。
2.当我们访问某一个结点值的时候,要把这个访问的结点放到某一头说明刚被访问过,而我们删除某一结点的时候需要从另一头删除,这有点像结点的访问频度排序问题,应该想到初学数据结构链表的“结点的访问频度”那道题,为了使得结点能够方便转移,我们使用双端链表。
3.基于以上两点,我们可以确定存储数据的结构为双端链表,同时额外定义一个map,key值为数据的key值,value值为对应双端链表的结点。
4.定义一个伪头结点和一个伪尾结点,在双端链表为空的时候是有这两个结点,更方便我们操作结点。
5.题目中可能讲的不太清楚,put其实包含了get的含义,因此当我们put一个结点,不管是修改值还是新增结点都把它放到双端链表的头部,当容量满了以后从链表的尾部删除结点,满足题目描述的“最近最少使用”思想。
代码
这道题的代码编写起来像一个小型项目了,因此我们分解着来一点点看。
1.双端链表的定义
struct DLinkedNode{
int key,value;
DLinkedNode *pre;
DLinkedNode *next;
DLinkedNode():key(0),value(0),pre(nullptr),next(nullptr) {}
DLinkedNode(int _key,int _value):key(_key),value(_value),pre(nullptr),next(nullptr) {}
};
其实就是在单链表的定义上进行了扩展,结点中的数据有key和value,这里构造函数的写法是简略版,比如第一个DLinkedNode():key(0),value(0),pre(nullptr),next(nullptr) {}相当于
DLinkedNode(){
key=0;
value=0;
pre=nullptr;
next=nullptr;
}
2.全局结构定义
根据前面的分析,我们需要定义一个map,用来查找key对应的结点,以及伪头结点、伪尾结点,还有链表的容量capacity,当前结点数num。这些作为类中的private值。
private:
unordered_map<int,DLinkedNode*> m;
DLinkedNode *Head; //伪头结点
DLinkedNode *Tail; //伪尾结点
int capacity; //链表容量
int num; //当前结点数
3.LRUCache构造函数的定义
在结点为空的时候应有伪头结点和伪尾结点相互指向,因此构造函数定义为:
LRUCache(int _capacity):capacity(_capacity),num(0) {
Head= new DLinkedNode(); //空伪头部
Tail= new DLinkedNode(); //空伪尾部
Head->next=Tail;
Tail->pre=Head;
}
注意这里capacity(_capacity),num(0)的写法相当于{}里capacity=_capacity;num=0;
链表为空时如下图所示:
4.一些辅助函数定义
(1)删除结点函数,画出结构图,按照序号执行。
void removeNode(DLinkedNode *node){
node->pre->next=node->next; //(1)
node->next->pre=node->pre; //(2)
}
(2)结点添加到头部函数。
void addToHead(DLinkedNode *node){
node->pre=Head; //(1)
node->next=Head->next; //(2)
Head->next->pre=node; //(3)
Head->next=node; //(4)
}
至于为什么是上面的顺序,其实在数据结构基础课的时候应该推导过,这样不会让链接断掉。
(3)结点移动到头部函数,这就是上面两个函数结合,先把结点摘下来,再添加到头部。
void moveToHead(DLinkedNode *node){
removeNode(node); //摘下结点
addToHead(node); //添加至头部
}
(4)删除结点函数,根据分析,最久未访问的结点在尾部,因此通过伪尾结点可以轻松找到并删除。
DLinkedNode* removeTail(){
DLinkedNode* node=Tail->pre; //最后一个结点是伪尾结点前一个
removeNode(node);
return node;
}
5.get和put方法定义
有了上述辅助函数,get和put方法的定义就比较轻松了。
(1)get方法
get就是根据输入的key,到定义的索引map中查找value,如果查找到则移动到头部。
int get(int key) {
if(m.count(key)==0) //未查找到
return -1;
DLinkedNode *node=m[key];
moveToHead(node); //将刚访问的结点放到头部
return node->value;
}
(2)put方法
在执行put时看一下key对应的结点是否存在,如果存在修改value,不存在再考虑是否超越存量。
void put(int key, int value) {
if(m.count(key)==0){
// key不存在,创建新结点
DLinkedNode *node = new DLinkedNode(key,value);
m[key]=node; //添加到map
addToHead(node); //添加到头部
num++;
if(num>capacity){ //超出容量从尾部删除(map和链表都要删除)
DLinkedNode *rem=removeTail();
m.erase(rem->key);
delete rem;
num--;
}
}else{
//key存在,修改node的值
DLinkedNode* node=m[key];
node->value=value;
moveToHead(node);
}
}
最后奉上完整代码:
struct DLinkedNode{
int key,value;
DLinkedNode *pre;
DLinkedNode *next;
DLinkedNode():key(0),value(0),pre(nullptr),next(nullptr) {}
DLinkedNode(int _key,int _value):key(_key),value(_value),pre(nullptr),next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int,DLinkedNode*> m;
DLinkedNode *Head;
DLinkedNode *Tail;
int capacity;
int num;
public:
LRUCache(int _capacity):capacity(_capacity),num(0) {
Head= new DLinkedNode(); //空伪头部
Tail= new DLinkedNode(); //空伪尾部
Head->next=Tail;
Tail->pre=Head;
}
int get(int key) {
if(m.count(key)==0) //未查找到
return -1;
DLinkedNode *node=m[key];
moveToHead(node); //将刚访问的结点放到头部
return node->value;
}
void put(int key, int value) {
if(m.count(key)==0){
// key不存在,创建新结点
DLinkedNode *node = new DLinkedNode(key,value);
m[key]=node; //添加到map
addToHead(node); //添加到头部
num++;
if(num>capacity){ //超出容量进行删除
DLinkedNode *rem=removeTail();
m.erase(rem->key);
delete rem;
num--;
}
}else{
//key存在,修改node的值
DLinkedNode* node=m[key];
node->value=value;
moveToHead(node);
}
}
void removeNode(DLinkedNode *node){
node->pre->next=node->next;
node->next->pre=node->pre;
}
void addToHead(DLinkedNode *node){
node->pre=Head;
node->next=Head->next;
Head->next->pre=node;
Head->next=node;
}
void moveToHead(DLinkedNode *node){
removeNode(node); //摘下结点
addToHead(node); //添加至头部
}
DLinkedNode* removeTail(){
DLinkedNode* node=Tail->pre; //最后一个结点是尾结点前一个
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);
*/