✍️ 作者:王文峰(外号:神秘小峯 / 山峯)|中国国学、传统文化与代码爱好者的程序人生,期待你的关注和支持!
Java 算法分层刷题:6 道高频题带「标准答案 + 追问链路 + 踩坑点」【两数之和、LRU、接雨水、单调队列】

📚 算法题最大的坑不是"不会做",而是做完就忘、问深就懵。本文按三层难度精选 6 道 Java 面试最高频题,每题固定三件套:标准答案(可直接跑)→ 追问链路(面试官的连环套怎么接)→ 高频踩坑点(别人挂在这的雷)。刷完这 6 题,哈希、栈、滑动窗口、双指针、单调队列、设计题六大考点全部打通。
一、开篇:为什么要"分层"刷题
| 层级 | 定位 | 目标用时 | 本篇题目 | 训练目标 |
|---|---|---|---|---|
| L1 入门 | 热身,建立手感 | 20~30 分钟/题 | 两数之和、有效括号 | 哈希表、栈的基本直觉 |
| L2 进阶 | 中频主力 | 40~60 分钟/题 | 最长无重复子串、LRU | 滑动窗口、设计题套路 |
| L3 高手 | 拉开差距 | 60 分钟+ | 接雨水、滑动窗口最大值 | 双指针证明、单调队列 |
💡 分层的意义:L1 建立"数据结构直觉",L2 训练"套路识别",L3 锻炼"证明与追问"。面试现场 80% 的题在 L2 水平,但决定你过不过的往往是追问环节——这就是每题都要配追问链路的原因。
刷题姿势:先自己想 15~20 分钟 → 再看标准答案 → 隔天盲写。看答案 ≠ 会做,盲写才算掌握。

二、L1-1:两数之和(哈希表入门)
题目:给定整数数组 nums 和目标值 target,找出和为 target 的两个数的下标,假设恰好一个答案。
标准答案
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>(); // 值 -> 下标
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
// 先查再放:避免"自己配自己"
if (seen.containsKey(need)) {
return new int[]{seen.get(need), i};
}
seen.put(nums[i], i);
}
return new int[0]; // 无解返回空数组,别返回 null
}
}
时间 O(n),空间 O(n)。
追问链路
| 追问 | 参考答案 |
|---|---|
| Q1:为什么一遍遍历就够? | 第 i 个元素只需知道"前面有没有 target-nums[i]“,哈希表 O(1) 查"已见过的值” |
| Q2:能不能先排序再双指针? | 可以降到 O(1) 空间,但排序会破坏原始下标,需额外记录 (值,下标) 对 |
| Q3:如果数组有重复值、要返回所有不重复对呢? | 排序 + 双指针 + 跳过重复值,答案去重 |
| Q4:哈希冲突时复杂度还成立吗? | 严格说退化为 O(n²),Java 8 后链表转红黑树,实际可忽略 |
高频踩坑点
- ❌ 先
put再查:[3,3], target=6会把自己当下标配对; - ❌ 暴力两层循环 O(n²) 起手——面试直接减分;
- ❌ 返回
null,建议返回空数组并说明约定。
三、L1-2:有效的括号(栈入门)
题目:()、[]、{} 视为有效,判断字符串是否全部有效嵌套。
标准答案
class Solution {
public boolean isValid(String s) {
if (s.length() % 2 != 0) return false; // 奇数必无效,快速返回
Deque<Character> stack = new ArrayDeque<>();
Map<Character, Character> pairs = Map.of(')', '(', ']', '[', '}', '{');
for (char c : s.toCharArray()) {
if (!pairs.containsKey(c)) { // 左括号入栈
stack.push(c);
} else { // 右括号:核对栈顶
if (stack.isEmpty() || stack.pop() != pairs.get(c)) {
return false;
}
}
}
return stack.isEmpty(); // 最终必须栈空
}
}
时间 O(n),空间 O(n)。
追问链路
| 追问 | 参考答案 |
|---|---|
| Q1:为什么非用栈不可? | 后进先出匹配"最近未闭合"的括号,这是栈的定义级场景 |
| Q2:只有一种括号能更省吗? | 用计数器代替栈,O(1) 空间 |
| Q3:进阶——生成所有合法括号组合? | 回溯:左括号数 < n 时可加左,右括号数 < 左括号数时可加右(经典 LeetCode 22) |
Q4:栈用 Stack 还是 ArrayDeque? | 永远 ArrayDeque,Stack 继承自 Vector,方法全加锁 |
高频踩坑点
- ❌ 结束只返回"遍历完了",忘了
stack.isEmpty()(漏(((这种情况); - ❌
Map.of只放右括号映射左括号,方向写反; - ❌ 用
Vector/Stack,性能与规范双扣分。
四、L2-1:最长无重复子串(滑动窗口)
题目:找出字符串中不含重复字符的最长子串长度。
标准答案
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> last = new HashMap<>(); // 字符 -> 最后出现下标
int left = 0, max = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 关键判断:旧位置必须在窗口内才需要收缩
if (last.containsKey(c) && last.get(c) >= left) {
left = last.get(c) + 1; // 左边界只增不减
}
last.put(c, right);
max = Math.max(max, right - left + 1);
}
return max;
}
}
时间 O(n),空间 O(字符集大小)。
追问链路
| 追问 | 参考答案 |
|---|---|
| Q1:为什么窗口不会"回退"? | left 只接收更大的值,窗口单调向右,所以是 O(n) 而非 O(n²) |
| Q2:定长窗口(长度 k 无重复)怎么改? | 删掉 left = last.get(c)+1,改为"窗口超 k 就左移",多一个长度校验 |
| Q3:变体——最长全 1 子数组(最多翻转 K 个 0)? | 不定长滑动窗口:零数 > K 时收缩左边界 |
| Q4:字符集很大(含中文/Emoji)怎么办? | HashMap 换成 int[128](ASCII)或直接 HashMap<Character,Integer> 兜底 |
高频踩坑点
- ❌ 少了
last.get(c) >= left判断:"abba"里left会被旧位置拉回去(经典翻车现场); - ❌ 用
substring+HashSet每次重建,O(n²) 超时; - ❌ 空串返回
-1,正确是0。
五、L2-2:LRU 缓存(设计题必考)
题目:实现 LRUCache,get/put 均要求 O(1)。
标准答案(标准库 5 行版)
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // 第三个参数 accessOrder=true:访问即挪到队尾
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1); // get 会触发 accessOrder 更新顺序
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity; // 超容自动淘汰最久未用
}
}
追问链路:手写版(面试标准要求)
| 追问 | 参考答案 |
|---|---|
| Q1:为什么能 O(1)? | 哈希表 O(1) 查节点 + 双向链表 O(1) 增删挪位,两者缺一不可 |
| Q2:为什么必须双向链表? | 单链表删除要先找前驱,找前驱 O(n);双向链表持有 prev 直接删 |
| Q3:边界判断很烦怎么办? | 哨兵头尾节点(dummy head/tail),所有节点操作统一化 |
| Q4:线程不安全怎么办? | Collections.synchronizedMap 或 ConcurrentHashMap + 分段锁;高并发常见"分片 LRU" |
手写版完整代码(背这个):
class LRUCache2 {
private final int capacity;
private final Map<Integer, Node> map = new HashMap<>();
private final Node head = new Node(0, 0), tail = new Node(0, 0); // 哨兵
static class Node {
int key, val; // key 必须存!淘汰时靠它从 map 里删
Node prev, next;
Node(int k, int v) { key = k; val = v; }
}
public LRUCache2(int capacity) {
this.capacity = capacity;
head.next = tail; tail.prev = head;
}
public int get(int key) {
Node n = map.get(key);
if (n == null) return -1;
moveToHead(n);
return n.val;
}
public void put(int key, int value) {
Node n = map.get(key);
if (n == null) {
n = new Node(key, value);
map.put(key, n);
addToHead(n);
if (map.size() > capacity) { // 淘汰队尾
Node removed = removeTail();
map.remove(removed.key); // 两处都要删!
}
} else {
n.val = value;
moveToHead(n);
}
}
private void addToHead(Node n) { n.prev = head; n.next = head.next; head.next.prev = n; head.next = n; }
private void removeNode(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }
private void moveToHead(Node n) { removeNode(n); addToHead(n); }
private Node removeTail() { Node n = tail.prev; removeNode(n); return n; }
}
高频踩坑点
- ❌ 忘了
accessOrder=true——写完发现是"纯插入序 LRU"; - ❌ 哈希表里存值不存 Node——删除节点时找不到引用,O(1) 变 O(n);
- ❌
Node不存key——淘汰时不知道该从 map 删哪个键; - ❌ 只删链表不删 map(或反之),两处必须同步。
六、L3-1:接雨水(双指针)
题目:给定 n 个非负整数表示柱子高度,计算按此排列的柱子下雨后能接多少雨水。
标准答案
class Solution {
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0, ans = 0;
while (left < right) {
if (height[left] < height[right]) { // 短边决定水量
leftMax = Math.max(leftMax, height[left]);
ans += leftMax - height[left];
left++;
} else {
rightMax = Math.max(rightMax, height[right]);
ans += rightMax - height[right];
right--;
}
}
return ans;
}
}
时间 O(n),空间 O(1)。
追问链路
| 追问 | 参考答案 |
|---|---|
| Q1:暴力/朴素做法是什么? | 每个位置 min(左侧最大, 右侧最大) - 当前高度,预处理两个最大值数组,O(n) 空间 |
| Q2:双指针为什么不会漏? | 某侧柱子矮于另一侧时,它那一侧的 max 已被"更远的边界"锁死,短边的水量只取决于本侧 max,不会受另一侧未来影响 |
| Q3:相等时走哪边? | 走哪边都对(else 分支),水量一样 |
| Q4:变体——接雨水 II(二维平面)? | 优先队列(小顶堆)从边界向内推进,每次弹出最低边扩展邻居(LeetCode 407) |
| Q5:兄弟题? | 柱状图最大矩形(单调栈,LeetCode 84),面试同样高频 |
高频踩坑点
- ❌ 边缘柱子算进水量(边缘不可能积水,正确算法天然为 0);
- ❌ 用
Math.max(0, leftMax - h)打补丁——说明逻辑已经错了,先想清楚再写; - ❌ 起手写暴力 O(n²) 却不主动说"可优化到 O(n)",会错失加分。
七、L3-2:滑动窗口最大值(单调队列)
题目:大小为 k 的窗口从左滑到右,返回每个窗口的最大值数组。
标准答案
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k <= 0 || nums.length < k) return new int[0];
int n = nums.length;
int[] ans = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // 存【下标】,对应值单调递减
for (int right = 0; right < n; right++) {
// 队尾比新元素小的全部出队:它们永远不可能成为最大值
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[right]) {
dq.pollLast();
}
dq.offerLast(right);
if (right >= k - 1) { // 窗口首次形成
ans[right - k + 1] = nums[dq.peekFirst()]; // 队首即窗口最大
if (dq.peekFirst() <= right - k) { // 队首已滑出窗口
dq.pollFirst();
}
}
}
return ans;
}
}
时间 O(n)(均摊),空间 O(k)。
追问链路
| 追问 | 参考答案 |
|---|---|
| Q1:为什么队列单调递减? | 队首永远是"活着的最大值",比新元素小的旧元素没有存在价值,直接淘汰 |
| Q2:为什么复杂度是 O(n)? | 每个下标最多入队一次、出队一次,均摊 O(1)——这是单调队列的证明核心 |
| Q3:为什么存下标不存值? | 靠下标判断"是否滑出窗口"(peekFirst() <= right - k),存值就没法判过期 |
| Q4:堆(优先队列)能做吗? | 能,O(n log n),堆顶过期时惰性删除——但打不过单调队列 |
| Q5:变体? | 窗口大小不固定求最大(不定长)、IPO 求最大利润(双堆)、数据流中位数(大小顶堆对) |
高频踩坑点
- ❌ 队列里存值——过期判断无从下手;
- ❌ 出队条件写成
<而非<=:相等元素残留,队列膨胀、答案可能取到过期元素; - ❌ 过期判断放在输出之前:队首可能是刚入队的合法元素,顺序要"先取最大,再清过期";
- ❌ 忘了边界:
k > n或k <= 0直接返回空数组。
八、六题一图流:数据结构与套路速查
| 题目 | 核心结构 | 时间 | 一句话套路 |
|---|---|---|---|
| 两数之和 | HashMap | O(n) | "见过什么"用哈希记 |
| 有效括号 | ArrayDeque 栈 | O(n) | "最近未匹配"交给栈 |
| 最长无重复子串 | 滑动窗口 + HashMap | O(n) | 不定长窗口:右扩左收 |
| LRU | HashMap + 双向链表 | O(1) | 查得快靠哈希,删得快靠双链 |
| 接雨水 | 双指针 | O(n) | 短边决定水量,向内推进 |
| 窗口最大值 | 单调队列 | O(n) | 没价值的元素提前踢掉 |
三板斧总结:见到"子串/子数组"想滑动窗口,见到"配对/去过的历史"想哈希或栈,见到"滑动中取最值"想单调队列,见到 O(1) 要求想哈希 + 链表/双指针。
九、刷题方法论:三遍法
- 第一遍:裸想 15~20 分钟——没思路就看题解,但要标记"卡点在哪一步";
- 第二遍:精读标准答案——重点看追问链路里的"为什么这么想",而不是背代码;
- 第三遍:隔天盲写——不看答案重写,写完跑用例,卡壳的地方就是没掌握的地方。
💡 加分动作:每题写完主动报复杂度并说一句"还有 XX 优化空间"——这一个动作能让你在面试官眼里从"背题的"变成"懂题的"。
十、总结与自测清单
- ✅ 两数之和:先查后放,别把自己配给自己;
- ✅ 有效括号:结束必须
stack.isEmpty(),容器用ArrayDeque; - ✅ 滑动窗口:
last.get(c) >= left是灵魂判断,窗口只进不退; - ✅ LRU:哈希存 Node 引用、Node 存 key、链表 map 两处同步;
- ✅ 接雨水:短边决定水量,能主动讲出双指针正确性证明;
- ✅ 窗口最大值:单调队列存下标,均摊 O(n) 的证明是"每个元素进出各一次";
- ✅ 任何题答完都补一句复杂度与优化方向。
下一篇:《Java 算法分层刷题·动态规划篇》——从爬楼梯到打家劫舍到最长公共子序列,把 DP 的状态定义与转移方程讲透,敬请关注!
🎯 学习教程(传送门)
| 序号 | 专栏 | 学习内容 | 当前状态 |
|---|---|---|---|
| 01 | JAVA | 从入门到进阶的系统知识 | 持续写作中…… |
| 02 | Oracle | 数据库从用法到实战 | 创作中…… |
| 03 | vbs脚本 | 手把手教你做脚本 | 完善中…… |
| 04 | IDEA | 牛逼哄哄的编程利器 | 编写中…… |
| 05 | 面试 | 吐血整理的面试技巧 | 更新中…… |

🔥 往期文章
🧭 精选往期好文,更多请移步 我的博客主页

👍 点赞 + ⭐ 收藏 + 💬 评论,一键三连走起!
你们的支持,是我持续创作的最大动力~

✨ 以上就是今天的全部内容,关注我,技术之路不迷路!
📖 本人详解
✍️ 作者:王文峰,参与 CSDN 2020 年度博客之星评选,《Java王大师王天师》,外号:神秘小峯 / 山峯
🏮 中国国学、传统文化与代码爱好者的程序人生,期待你的关注和支持!
🔗 转载说明:务必注明来源(注明:作者:王文峰哦)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Feng_wwf/article/details/168531047




