LeetCode 👉 HOT 100 👉 LRU 缓存 - 中等题

1,130 阅读4分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。

题目

请你设计并实现一个满足 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) 的平均时间复杂度运行。

示例

输入
["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

  • 2、可以通过 get(key) 方法返回对应的值

  • 3、可以通过 put(key, value) 存下对应数据

  • 4、当调用 put 方法时,如果超出最大存储量 capacity,则需要将 最近最少使用 的数据删除

关键点在第4点,什么叫 最近最少使用,按照题目意思应该为:如果一个数据被使用了,无论是 get ,还是 put,那么它在所有数据中的排序,应该移动到最前面;那么每次需要删除数据时,应该从排序中,选择排序最尾部的数据。

如果不考虑 get、set 的操作时间复杂度的问题,Js语言中,有一个 Map 的数据结构刚好符合上面的特性:Map 里面的数据是有序的,按照每次插入的顺序向后排列;按照题意,可以实现如下:

  • 1、实现 get:如果缓存中不存在,则返回 -1;如果存在,先获取值 value,再从缓存中删除该 key,再插入该 key, value 数据,最后返回 value;这样做的目的,是为了将该数据的位置,刷新到最前面

  • 2、实现 push:如果缓存中出在该 key,先删除;再将数据放入缓存中(为了刷新该数据的位置);最后判断缓存数据的长度,是否大于最大存储量,满足的话,利用 Map.keys().next().value 获取对应的 最近最少使用 的数据 key,删除该数据

关于 .next() 方法,可以参考 MDN 对于该Api的解释,传送门 ☞

    /**
     * @param {number} capacity
     */
    var LRUCache = function (capacity) {
        this.map = new Map();
        this.capacity = capacity;
    };

    /** 
     * @param {number} key
     * @return {number}
     */
    LRUCache.prototype.get = function (key) {
        if(this.map.has(key)) {
            let value = this.map.get(key);
            this.map.delete(key);
            this.map.set(key, value);
            return value;
        } else {
            return -1;
        }
    };

    /** 
     * @param {number} key 
     * @param {number} value
     * @return {void}
     */
    LRUCache.prototype.put = function (key, value) {
        if(this.map.has(key)) {
            this.map.delete(key);
        }
        this.map.set(key, value);
        if(this.map.size > this.capacity) {
            this.map.delete(this.map.keys().next().value);
        }
    };

思考: 如果要实现题目要求的 getput 必须以 O(1) 的平均时间复杂度运行,那么就要用到 双向链表 + 哈希表,为什么:

  • 1、对于 get 方法,如果想达到时间复杂度为 O(1)哈希表 是满足条件的。

  • 2、因为存在最大容量的限制,在 put 过程中必然存在超过数量时,要进行删除操作

  • 3、对于删除操作:

    • 数组结构,插入、删除操作的时间复杂度都为 O(n),不可取
    • 单向链表,在删除时,需要找到当前节点的前序节点,花费 O(n),不可取
    • 双向链表,获取前序节点的时间为 O(1),删除也只需要将前序节点的下一节点指向当前节点的下一节点即可

具体实现如下:

    // 定义双向链表结构
    function ListNode(key, value) {
        this.key = key;
        this.value = value;
        this.next = null;
        this.prev = null;
    }

    /**
     * @param {number} capacity
     */
    var LRUCache = function (capacity) {
        this.hash = {}; // 哈希表
        this.capacity = capacity;
        this.size = 0;

        // 默认头、尾节点
        this.root = new ListNode('start', 'start');
        this.end = new ListNode('end', 'end');
        this.root.next = this.end;
        this.end.prev = this.root;

        // 从链表删除一个节点
        this.removeNode = node => {
            const prev = node.prev;
            const next = node.next;
            prev.next = next;
            next.prev = prev;
        }

        // 把节点添加到头部
        this.addToHead = node => {
            const head = this.root.next;
            this.root.next = node;
            node.prev = this.root;
            node.next = head;
            head.prev = node;
        }

        // 把节点移动到头部
        // 先删除,后移动
        this.addHead = node => {
            this.removeNode(node);
            this.addToHead(node);
        }

        // 移除最后一个节点
        this.popEnd = () => {
            let node = this.end.prev;
            this.removeNode(node);
            return node;
        }

        // 删除缓存的item 数据
        this.removeLRUItem = () => {
            let node = this.popEnd();
            delete this.hash[node.key];
            this.size--;
        }
    };

    /** 
     * @param {number} key
     * @return {number}
     */
    LRUCache.prototype.get = function (key) {
        
        const node = this.hash[key];

        if(!node) return -1;
        this.addHead(node);
        return node.value;
    };

    /** 
     * @param {number} key 
     * @param {number} value
     * @return {void}
     */
    LRUCache.prototype.put = function (key, value) {
        let node = this.hash[key];
        if(!node) {

            if(this.size == this.capacity) {
                this.removeLRUItem();
            }

            node = new ListNode(key, value);
            this.addToHead(node);
            this.hash[key] = node;
            this.size++;

        } else {
            node.value = value;
            this.addHead(node);
        }
    };

小结

双向链表的特性为,从其任意一个节点开始,可以很方便的获取它的前驱节点和后继节点,所以对于删除操作,双向链表的时间复杂度可以降为 O(1)

LeetCode 👉 HOT 100 👉 LRU 缓存 - 中等题

合集:LeetCode 👉 HOT 100,有空就会更新,大家多多支持,点个赞👍

如果大家有好的解法,或者发现本文理解不对的地方,欢迎留言评论 😄