【面经 每日一题】面试题16.25.LRU缓存(medium)

每日一题

面试题16.25.LRU缓存

一张图秒懂 LRU!

问:需要几个哨兵节点?

答:一个就够了。一开始哨兵节点 dummy 的 prev 和 next 都指向 dummy。随着节点的插入,dummy 的 next 指向链表的第一个节点(最上面的书),prev 指向链表的最后一个节点(最下面的书)。

问:为什么节点要把 key 也存下来?

答:在删除链表末尾节点时,也要删除哈希表中的记录,这需要知道末尾节点的 key。

代码

java 复制代码
public class LRUCache {
    private static class Node {
        int key, value;
        Node prev, next;

        Node(int k, int v) {
            key = k;
            value = v;
        }
    }

    private final int capacity;
    private final Node dummy = new Node(0, 0); // 哨兵节点
    private final Map<Integer, Node> keyToNode = new HashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        dummy.prev = dummy;
        dummy.next = dummy;
    }

    public int get(int key) {
        Node node = getNode(key);
        return node != null ? node.value : -1;
    }

    public void put(int key, int value) {
        Node node = getNode(key);
        if (node != null) { // 有这本书
            node.value = value; // 更新 value
            return;
        }
        node = new Node(key, value); // 新书
        keyToNode.put(key, node);
        pushFront(node); // 放在最上面
        if (keyToNode.size() > capacity) { // 书太多了
            Node backNode = dummy.prev;
            keyToNode.remove(backNode.key);
            remove(backNode); // 去掉最后一本书
        }
    }

    private Node getNode(int key) {
        if (!keyToNode.containsKey(key)) { // 没有这本书
            return null;
        }
        Node node = keyToNode.get(key); // 有这本书
        remove(node); // 把这本书抽出来
        pushFront(node); // 放在最上面
        return node;
    }

    // 删除一个节点(抽出一本书)
    private void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
    }

    // 在链表头添加一个节点(把一本书放在最上面)
    private void pushFront(Node x) {
        x.prev = dummy;
        x.next = dummy.next;
        x.prev.next = x;
        x.next.prev = x;
    }
}

复杂度分析

时间复杂度:所有操作均为 O(1)。

空间复杂度:O(min(p,capacity)),其中 p 为 put 的调用次数。

注意:

内部类(如Node)声明为static

在Java中,内部类(如Node)若声明为static,则不持有外部类(LRUCache)的引用。如果Node是非static(如原代码中的public class Node),则每个Node对象会隐式关联到一个LRUCache实例,导致:

内存泄漏风险:当LRUCache实例被销毁后,Node对象因持有外部类引用而无法被垃圾回收。

不必要的依赖:Node本身仅需存储键值和链表指针,无需访问LRUCache的实例字段(如capacity或dummy)。

思考引导:

如果Node是非static,当调用LRUCache cache = new LRUCache(10);后,cache被设为null,但Node对象仍因持有cache引用而无法回收。这会导致什么后果?

如果Node是非static(即默认内部类),当LRUCache实例被销毁后,Node对象仍隐式持有该实例的引用,导致垃圾回收器无法回收Node对象。后果是:内存持续泄漏,应用占用内存不断增长,最终可能引发OutOfMemoryError(内存溢出)。

(例如:频繁创建/销毁LRUCache实例时,内存泄漏会加速系统崩溃。)

相关推荐
颜酱20 分钟前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法
Derek_Smart1 小时前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot
NE_STOP2 小时前
MyBatis-mybatis入门与增删改查
java
Lee川3 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i5 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
孟陬5 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
想用offer打牌5 小时前
一站式了解四种限流算法
java·后端·go
绝无仅有5 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有6 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
华仔啊6 小时前
Java 开发千万别给布尔变量加 is 前缀!很容易背锅
java