题目链接: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) 的平均时间复杂度运行。
示例:
            
            
              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 <= 30000 <= key <= 100000 <= value <= 105- 最多调用 
2 * 105次get和put 
翻译一下:你往桌面上堆书,并且每本书都有自己的编号,你最多能堆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;
        }
    }
};