LeetCodes刷题总结7——LRU缓存

题目

146. LRU 缓存 - 力扣(LeetCode)

请你设计并实现一个满足  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);
 */

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值