【LeetHOT100】LRU缓存——Java多解法详解

一、题目描述

146. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存

  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1

  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity,则应该逐出最久未使用的关键字

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

示例:

text

复制代码
输入:
["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 <= 10^4

  • 0 <= value <= 10^5

  • 最多调用 2 * 10^5getput

二、解题思路概览

LRU 缓存的核心需求:

  1. 快速查找:根据 key 找到对应的 value(需要 HashMap)

  2. 快速更新访问顺序:每次 get/put 都要将该 key 标记为最近使用(需要能快速移动节点)

  3. 快速删除最久未使用:当容量满时,删除链表尾部的节点(需要能快速删除尾部)

满足 O(1) 操作的数据结构组合:HashMap + 双向链表

解法 时间复杂度 空间复杂度 特点 面试推荐
双向链表 + HashMap O(1) O(capacity) 标准解法,面试必考 ⭐⭐⭐⭐⭐
LinkedHashMap O(1) O(capacity) 利用 Java 内置类,代码极简 ⭐⭐⭐⭐
单链表 + HashMap O(1) 查,O(n) 删 O(capacity) 不符合 O(1) 要求

三、解法一:双向链表 + HashMap(标准解法)⭐⭐⭐⭐⭐

3.1 核心思路

  • 双向链表:维护 key 的访问顺序。头部是最近使用的,尾部是最久未使用的

  • HashMap:存储 key 到链表节点的映射,实现 O(1) 查找

3.2 数据结构设计

java

复制代码
// 双向链表节点
class Node {
    int key;
    int value;
    Node prev;
    Node next;
    
    Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

// LRU 缓存主类
class LRUCache {
    private Map<Integer, Node> map;
    private Node head;  // 伪头节点,head.next 是真正的头
    private Node tail;  // 伪尾节点,tail.prev 是真正的尾
    private int capacity;
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        Node node = map.get(key);
        // 移到头部
        moveToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            // 更新值并移到头部
            Node node = map.get(key);
            node.value = value;
            moveToHead(node);
        } else {
            // 如果容量已满,删除尾部节点
            if (map.size() == capacity) {
                Node tailNode = removeTail();
                map.remove(tailNode.key);
            }
            // 创建新节点,添加到头部
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            addToHead(newNode);
        }
    }
    
    // 将节点移动到头部
    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }
    
    // 从链表中删除节点
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    // 在头部添加节点
    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    // 删除尾部节点并返回
    private Node removeTail() {
        Node tailNode = tail.prev;
        removeNode(tailNode);
        return tailNode;
    }
}

3.3 图解示例

capacity = 2 为例:

text

复制代码
初始: head <-> tail

put(1,1):
head <-> 1 <-> tail

put(2,2):
head <-> 2 <-> 1 <-> tail

get(1):
head <-> 1 <-> 2 <-> tail

put(3,3): 容量满,删除尾部2
head <-> 3 <-> 1 <-> tail

get(2): 返回 -1

put(4,4): 容量满,删除尾部1
head <-> 4 <-> 3 <-> tail

3.4 复杂度分析

  • 时间复杂度:O(1),所有操作都是常数时间

  • 空间复杂度:O(capacity),存储最多 capacity 个节点

四、解法二:LinkedHashMap(Java 内置实现)⭐⭐⭐⭐

4.1 核心思路

LinkedHashMap 本身就是一个 HashMap + 双向链表,可以维护访问顺序。通过构造参数 accessOrder = true,每次 get/put 都会将节点移到链表末尾。

4.2 代码实现

java

复制代码
class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    
    public LRUCache(int capacity) {
        // accessOrder = true 表示按访问顺序排序
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        Integer value = super.get(key);
        return value == null ? -1 : value;
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
    
    // 当 size > capacity 时,删除最久未使用的节点(即链表头部的节点)
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

4.3 一行流写法(面试炫技,不推荐)

java

复制代码
class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    public void put(int key, int value) {
        super.put(key, value);
    }
    @Override protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

4.4 优缺点

优点 缺点
代码极简(不到20行) 面试官可能要求自己实现双向链表
利用成熟的内置类 无法展示对数据结构的理解
不易出错 有些公司面试禁止使用

五、常见错误与注意事项

5.1 常见错误

  1. 忘记更新双向链表的双向指针 :只更新了 prev 或只更新了 next

  2. removeNode 时未处理边界:使用伪头尾节点可以避免空指针

  3. put 已存在的 key 时,只更新 value 而未移到头部

  4. put 新节点时,先判断容量再添加,而不是先添加再判断

5.2 关键点总结

操作 要做的事
get ① 从 map 取节点 ② 移到头部 ③ 返回值
put(key 存在) ① 更新 value ② 移到头部
put(key 不存在) ① 若容量满,删除尾部节点 ② 创建新节点 ③ 添加到头部 ④ 放入 map

六、解法对比与总结

方法 代码量 可读性 面试推荐 适用场景
双向链表 + HashMap 较多 清晰 ⭐⭐⭐⭐⭐ 面试标准答案
LinkedHashMap 极少 极简 ⭐⭐⭐⭐ 快速实现,或说明思路后使用

七、面试建议

7.1 标准回答流程

  1. 分析需求:需要 O(1) 的 get 和 put,同时维护访问顺序

  2. 选择数据结构:HashMap(查找)+ 双向链表(维护顺序)

  3. 解释为什么双向链表

    • 删除任意节点 O(1)

    • 移动到头部 O(1)

    • 删除尾部 O(1)

    • 单链表无法 O(1) 删除任意节点(需要找前驱)

  4. 画图说明:伪头尾节点如何简化边界处理

  5. 写出代码

7.2 追问与扩展

Q:为什么不用单链表?

A:单链表删除任意节点需要 O(n) 时间找前驱,无法满足 O(1) 要求。

Q:为什么不用数组 + 时间戳?

A:get 操作需要 O(log n) 找最大值,且维护访问时间需要额外存储。

Q:LinkedHashMap 是怎么实现的?

A:它继承了 HashMap,内部维护了一个双向链表,通过 beforeafter 指针连接节点。

Q:如何实现 LFU(最不经常使用)缓存?

A:需要两个 HashMap + 双向链表/优先队列,实现更复杂。

八、相关链接

相关推荐
zx2859634001 小时前
Laravel 4.x:颠覆PHP框架的10大革新特性
开发语言·php·laravel
952361 小时前
SpringAOP
java·后端·学习·spring
浩冉学编程2 小时前
微信小程序中基于java后端实现官方的文本内容安全识别msgSecCheck
java·前端·安全·微信小程序·小程序·微信公众平台·内容安全审核
A__tao2 小时前
JSON 转 Java 实体类工具(支持嵌套与注释解析)
java·python·json
zx2859634002 小时前
Laravel6.x新特性全解析
java·后端·spring
极光代码工作室2 小时前
基于SpringBoot的图书管理系统
java·springboot·web开发·后端开发
许彰午2 小时前
# 从OOM到根治的完整过程——导出大数据的应急、根因分析与游标方案
java·大数据·数据库·系统架构
threelab2 小时前
Three.js 咖啡杯烟雾效果 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
上弦月-编程2 小时前
C语言指针超详细教程——从入门到精通(面向初学者)
java·数据结构·算法