力扣146LRU缓存

题目链接: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 ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

示例:

scss 复制代码
输入
["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 * 105getput

翻译一下:你往桌面上堆书,并且每本书都有自己的编号,你最多能堆capacity本书。get函数的功能就是让你可以直接通过编号找到对应的书籍,找到之后放在书堆的最上面。put函数就是先看看编号为key的书是否存在,如果不存在就往书堆最上面放一本;如果存在就将原本编号为key的书换掉,再放到书堆最上面。如果书堆满了,那么就需要优先将最下面的书移走。

现在put函数与get函数都要用O(1)的时间复杂度实现。

一开始想直接使用vector,先分配capacity的内存,每个元素值为-1(因为题目说value为非负数)。get函数就先直接用下标获取元素,但是问题来了:怎么把get到的书放在最上面?即我们怎么知道最后一个不是-1的元素在哪里?只能通过遍历。那时间复杂度就不是O(1)而是O(n)了。

可见问题在于预分配了空间。

那我们能不能干脆不预分配空间,用一个变量记录当前vector中元素的数量?这样一来整个vector里都是有效元素。但是问题又来了:如果我们把中间的元素移到了上面,那空出来的位置要补上,这就意味着必然要进行移动操作,那时间复杂度肯定不是O(n)了。

可见问题在于删除元素无法补位。

所以,使用vector肯定是不行了,更不用说队列和栈之类的。根据前面两个问题,可以自然而然想到链表。因为在链表中删除元素是O(1)操作,不涉及补位,也没有预分配空间的问题。

确定使用链表之后再分析:

get函数首先就要解决根据key找到value的问题,所以必须要有一个哈希表来维护键值对。其次要将这个key对应的元素移到链表的最前面。也就是说,除了要根据key值找到value的值,还要找到这个key值对应的节点。这也就意味着,哈希表的value值应该是节点指针而不是int,否则就无法进行移动操作。找到对应节点之后要将其移动到最前面,那这就意味着我们需要知道第二个节点的指针,这有点麻烦,因为第二个节点可能会变化。所以我们不妨搞个虚拟头节点,这样一来移动到头部相当于移动到虚拟头节点的后一位。

put函数首先要判断key是否存在,这个看哈希表即可。如果key存在,更改value即可,value为节点指针的好处就在这里。如果value只是int,那就没办法改变链表中节点的值了。然后我们将这个节点移动到链表头部。可以看到移动到链表头部这个操作使用了多次,因此可以将其单独封装成一个函数。如果key不存在,我们就new一个节点,然后把它放到最前面。如果哈希表的size已经比capacity要大了,就把最后一个节点从链表与哈希表中删除。我们也可以把删除操作封装成一个函数。

这时候新问题来了,怎么知道最后一个节点的key?不知道key就没办法将其从哈希表中删除。使用环形链表可以吗?并不行,因为只知道最后一个节点的next是虚拟头节点。所以要使用双向链表。并且,每个节点要存储key/value/next/prev,因为找到尾节点之后还要返回它的key值,这样才能从哈希表中删除。

另外,可以将由key找对应节点的操作封装为一个函数。在这个函数中,如果找得到节点就将其移到头部。

ini 复制代码
struct Node {
    int key;
    int value;
    Node* prev;
    Node* next;
​
    Node(int k = 0,int v = 0):key(k),value(v),prev(nullptr),next(nullptr){}
};
​
class LRUCache {
private:
    int cap;//容量
    Node* dummy_head;
    unordered_map<int, Node*>mp;
​
    //移动到头部
    void move_to_front(Node* node) {
        node->prev = dummy_head;
        node->next = dummy_head->next;//第一个加入的节点就是最后一个节点
        node->prev->next = node;
        node->next->prev = node;
    }
​
    //删除节点
    void del_node(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
​
    //获取key对应的节点并将其移动到头部
    Node* get_node(int key) {
        auto it = mp.find(key);
        if (it == mp.end()) {
            //没有这本书
            return nullptr;
        }
        Node* node = it->second;
        del_node(node);//要把书抽出来。不然如果只有dummy_head以及另一个节点node1,就会导致node1自己指向自己
        move_to_front(node);
        return node;
    }
​
public:
    LRUCache(int capacity) {
        cap = capacity;
        dummy_head = new Node();
        dummy_head->next = dummy_head;
        dummy_head->prev = dummy_head;
    }
​
    int get(int key) {
        Node* node = get_node(key);
        return node ? node->value : -1;
    }
​
    void put(int key, int value) {
        Node* node = get_node(key);
        if (node) {
            //节点存在,更新其value即可
            node->value = value;
            return;
        }
        //如果节点不存在,就new一个,并放到最上面
        node = new Node(key, value);
        mp[key] = node;
        move_to_front(node);
​
        //如果超出容量,删掉最后一个节点
        if (mp.size() > cap) {
            Node* del = dummy_head->prev;
            mp.erase(del->key);
            del_node(del);
            delete del;
        }
    }
};
相关推荐
南山安7 小时前
面试必考点: 深入理解CSS盒子模型
javascript·面试
TimelessHaze7 小时前
🧱 一文搞懂盒模型box-sizing:从标准盒到怪异盒的本质区别
前端·css·面试
绝无仅有7 小时前
某游戏大厂计算机网络面试问题深度解析(一)
后端·面试·架构
在下雨5997 小时前
条件变量与互斥锁复习
c++·面试
绝无仅有8 小时前
某游戏大厂分布式系统经典实战面试题解析
后端·面试·程序员
Baihai_IDP8 小时前
探讨超长上下文推理的潜力
人工智能·面试·llm
spmcor8 小时前
Vue命名冲突:当data和computed相爱相杀...
前端·面试
拉不动的猪8 小时前
单点登录中权限同步的解决方案及验证策略
前端·javascript·面试
007php00718 小时前
某游戏大厂 Java 面试题深度解析(四)
java·开发语言·python·面试·职场和发展·golang·php