【面经 每日一题】面试题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实例时,内存泄漏会加速系统崩溃。)

相关推荐
Flynt8 分钟前
Spring Boot 3开虚拟线程,我把生产服务搞崩了两次
java·spring boot
稚南城才子,乌衣巷风流10 分钟前
树的重心:概念、性质与算法详解
算法·深度优先·图论
不会就选b14 分钟前
算法日常・每日刷题--<哈希>1
算法·哈希算法
SimonKing17 分钟前
别再盲目跑测试了,用 JaCoCo 告诉你哪些代码根本没被覆盖
java·后端·程序员
沉默王二20 分钟前
携程员工:在携程4年,月薪3万左右,基本没涨薪了。不过挺满足的,毕竟稳定,福利也不错(附Agent面试题)
面试·openai·agent
Moment22 分钟前
2026 了,前端转 AI 全栈我是这么学的 😍😍😍
前端·后端·面试
zander25822 分钟前
LeetCode 207. 课程表
算法·深度优先
唐青枫23 分钟前
Java Grails 实战详解:用 Groovy 和 GORM 快速开发 Web 应用
java·groovy
SemiTris24 分钟前
Java 异常体系深度解析:从误区到精通
java
萌动的小火苗25 分钟前
深度学习中的损失函数与优化算法基础知识
人工智能·深度学习·算法