3
无重复字符的最长子串
中等高频
146
LRU缓存机制
中等高频
206
反转链表
简单高频
215
数组中的第K个最大元素
中等高频
25
K个一组翻转链表
困难高频
15
三数之和
中等高频
53
最大子数组和
中等高频
912
排序数组
中等高频
21
合并两个有序链表
简单高频
5
最长回文子串
中等高频
200
岛屿数量
中等高频
33
搜索旋转排序数组
中等高频
46
全排列
中等高频
88
合并两个有序数组
简单高频
20
有效的括号
简单高频
121
买卖股票的最佳时机
简单高频
236
二叉树的最近公共祖先
中等高频
92
反转链表 II
中等高频
103
二叉树的锯齿形层序遍历
中等高频
141
环形链表
简单高频
300
最长上升子序列
中等高频
54
螺旋矩阵
中等高频
143
重排链表
中等高频
23
合并K个排序链表
困难高频
415
字符串相加
简单高频
56
合并区间
中等高频
160
相交链表
简单高频
42
接雨水
困难高频
1143
最长公共子序列
中等高频
124
二叉树中的最大路径和
困难高频
93
复原IP地址
中等高频
82
删除排序链表中的重复元素 II
中等中频
19
删除链表的倒数第N个节点
中等中频
142
环形链表 II
中等中频
4
寻找两个正序数组的中位数
困难中频
199
二叉树的右视图
中等中频
102
二叉树的层序遍历
中等中频
165
比较版本号
中等中频
704
二分查找
简单中频
232
用栈实现队列
简单中频
22
括号生成
中等中频
94
二叉树的中序遍历
简单中频
239
滑动窗口最大值
困难中频
69
x 的平方根
简单中频
148
排序链表
中等中频
32
最长有效括号
困难中频
31
下一个排列
中等中频
8
字符串转换整数 (atoi)
中等中频
70
爬楼梯
简单中频
322
零钱兑换
中等中频
43
字符串相乘
中等中频
76
最小覆盖子串
困难中频
41
缺失的第一个正数
困难中频
105
从前序与中序遍历序列构造二叉树
中等中频
78
子集
中等中频
151
翻转字符串里的单词
中等中频
155
最小栈
简单中频
34
在排序数组中查找元素的第一个和最后一个位置
中等中频
394
字符串解码
中等中频
101
对称二叉树
简单中频
39
组合总和
中等中频
470
用 Rand7() 实现 Rand10()
中等低频
64
最小路径和
中等低频
104
二叉树的最大深度
简单低频
110
平衡二叉树
简单低频
144
二叉树的前序遍历
简单低频
48
旋转图像
中等低频
234
回文链表
简单低频
695
岛屿的最大面积
中等低频
122
买卖股票的最佳时机 II
简单低频
240
搜索二维矩阵 II
中等低频
221
最大正方形
中等低频
98
验证二叉搜索树
中等低频
543
二叉树的直径
简单低频
14
最长公共前缀
简单低频
179
最大数
中等低频
113
路径总和 II
中等低频
662
二叉树最大宽度
中等低频
62
不同路径
中等低频
198
打家劫舍
中等低频
152
乘积最大子数组
中等低频
560
和为K的子数组
中等低频
112
路径总和
简单低频
226
翻转二叉树
简单低频
209
长度最小的子数组
中等低频
227
基本计算器 II
中等低频
169
多数元素
简单低频
24
两两交换链表中的节点
中等低频
139
单词拆分
中等低频
283
移动零
简单低频
718
最长重复子数组
中等低频
1
两数之和
简单低频
2
两数相加
中等低频
#146
中等
高频
LRU 缓存模拟

LRU缓存机制

这是一道围绕设计、哈希表、链表展开的高频练习。建议先掌握「LRU 缓存模拟」这套写法,再结合下方步骤讲解理解状态维护、边界处理和复杂度取舍。

设计
哈希表
链表
双向链表

题目分析

你要设计一个固定容量的缓存。

当缓存没满时,可以继续插入;当缓存满了,再插入新元素时,必须把最近最少使用的那个元素淘汰掉。

它要支持两个操作:

  • get(key):查到就返回值,否则返回 -1
  • put(key, value):插入或更新键值对

只要一个元素被访问过,它就应该被视为“最近使用”。

一句话概括:

设计一个支持 O(1) 查找、更新、淘汰最久未使用元素的缓存结构。

接下来怎么看推荐代码: 带着这个理解再看推荐代码时,重点观察这条主线:用哈希表定位节点,用双向链表维护最近使用顺序,访问和写入都把节点移动到头部。

推荐代码

推荐解法:LRU 缓存模拟
时间复杂度: O(1)
空间复杂度: O(capacity)
核心思路: 用哈希表定位节点,用双向链表维护最近使用顺序,访问和写入都把节点移动到头部。

class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;

        public DLinkedNode() {
        }

        public DLinkedNode(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        } else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

结构化讲解

面试时怎么讲

开场思路

这题我会用哈希表加双向链表,把查找和顺序维护拆开处理。

核心过程

  1. 哈希表负责 key 到节点的 O(1) 定位。
  2. 双向链表头部表示最近使用,尾部表示最久未使用。
  3. 每次 get 或命中 put,都把节点移到头部。
  4. 插入新节点导致超容量时,淘汰尾部节点并同步删除哈希表记录。

复杂度总结

单次 get / put 时间复杂度 O(1),空间复杂度 O(capacity)

面试补一句:LRU 真正考的是“两个数据结构如何维持同一个状态”。

核心思路

LRU Cache 的难点在于同时满足两件事:按 key 快速找到节点,以及按最近使用顺序快速淘汰节点。哈希表和双向链表正好一快一序。

步骤讲解

1

哈希表和双向链表配合建模

哈希表负责从 key 直达节点,双向链表负责维护从“最近使用”到“最久未使用”的顺序。

为什么这样做:只靠哈希表无法淘汰最旧节点,只靠链表又做不到 O(1) 查找。
对应代码提示:Map<Integer, Node> cache = new HashMap<>();
2

访问或更新后都移动到头部

只要一个节点被 getput 访问,就把它移到链表头,表示它是最近刚使用过的。

为什么这样做:LRU 的核心就是“最近使用的留下,最久未使用的靠后”。
对应代码提示:remove(node); addToHead(node);
3

容量超限时淘汰尾部节点

插入新节点后,如果容量超出限制,就删除链表尾部前一个真实节点,并同步从哈希表移除。

为什么这样做:尾部节点就是当前最久未使用的那个元素。
对应代码提示:Node removed = removeTail(); cache.remove(removed.key);

易错点

只用哈希表,不维护使用顺序

这样虽然能 O(1) 找 key,但无法 O(1) 知道该淘汰谁。

正确理解:必须配合双向链表维护最近使用次序。

更新已有 key 时没移动到头部

这会导致刚访问过的节点仍然按旧顺序被淘汰。

正确理解:get 和命中的 put 都要执行“移到头部”。

淘汰节点时只删链表不删哈希表

缓存状态会不一致,后续查找可能拿到已经淘汰的节点。

正确理解:淘汰动作必须同时更新链表和哈希表。

复杂度与适用判断

时间复杂度:O(1)
空间复杂度:O(capacity)
比其他方案更好在哪里:比仅用数组或普通链表模拟更高效,满足题目对 O(1) 操作的硬要求。
适用判断:只要题目同时要求“按 key 快速访问”与“按最近使用顺序淘汰”,优先想到哈希表 + 双向链表。

额外提醒

  • 访问更新头部,淘汰删除尾部,是整个结构的两个核心动作。
动画演示仅在桌面端提供,移动端请优先阅读推荐代码与结构化讲解。