栈
本章题海战术
| 典型例题 | leetcode题目 | leetcode链接 |
|---|---|---|
| 有效的括号 | leetcode 20 | https://leetcode.cn/problems/valid-parentheses/description/ |
| 最小栈 | leetcode 155 | https://leetcode.cn/problems/min-stack/description/ |
| 每日温度 | leetcode 739 | https://leetcode.cn/problems/daily-temperatures/description/ |
| 逆波兰表达式求值 | leetcode 150 | https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/ |
| 柱状图中最大的矩形 | leetcode 84 | https://leetcode.cn/problems/largest-rectangle-in-histogram/description/ |
| 用栈实现一个队列 | leetcode 232 | https://leetcode.cn/problems/implement-queue-using-stacks/description/ |
栈
📚 栈的核心特性 & Java 实现规范
栈是 后进先出(LIFO) 的线性数据结构,所有插入、删除操作都只能在栈顶完成。
⚠️ 面试必踩基础坑:
- 不推荐使用
java.util.Stack:它继承自 Vector,方法默认加synchronized保证线程安全,性能差,且继承体系设计不规范。 - 大厂标准写法:使用
Deque接口,实现类优先选ArrayDeque(基于动态数组,读写性能最优)
Deque<Integer> stack = new ArrayDeque<>();栈操作示意图
✅ 高频题 1:有效的括号(LeetCode 20・简单)
题目:给定一个只包含 () [] {} 的字符串,判断括号是否有效。左括号必须用同类型右括号闭合,且顺序正确。
解题思路
- 遇到左括号,将对应的右括号压入栈
- 遇到右括号,判断栈是否为空、栈顶元素是否与当前右括号匹配
- 遍历结束后,栈为空则说明完全匹配
核心 Java 代码
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')');
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
// 遇到右括号,栈空或栈顶不匹配直接返回false
else if (stack.isEmpty() || stack.pop() != c) return false;
}
return stack.isEmpty();
}💡 考点点睛
- 栈的经典匹配场景:嵌套、对称类问题优先考虑栈
- 时间复杂度 O (n),空间复杂度 O (n)
📏 高频题 2:最小栈(LeetCode 155・中等)
题目:设计一个支持 push、pop、top 操作,并能在 O (1) 时间内检索到最小元素的栈。
解题思路
采用辅助栈同步主栈操作:
- 主栈存储正常元素
- 辅助栈存储「当前栈内的最小值」,每次 push 新元素时,比较新元素与辅助栈栈顶,将更小的值压入辅助栈
- pop 时两个栈同步弹出,辅助栈栈顶永远是当前栈的最小值
双栈结构示意图
核心 Java 代码
class MinStack {
Deque<Integer> mainStack;
Deque<Integer> minStack;
public MinStack() {
mainStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
mainStack.push(val);
// 辅助栈压入当前最小值
minStack.push(minStack.isEmpty() ? val : Math.min(val, minStack.peek()));
}
public void pop() {
mainStack.pop();
minStack.pop();
}
public int top() {
return mainStack.peek();
}
public int getMin() {
return minStack.peek();
}
}💡 考点点睛
- 空间换时间的经典设计题
- 进阶追问:如何优化辅助栈空间?(重复最小值只存一次,pop 时判断当前值是否等于最小值再弹出)
🌡️ 高频题 3:每日温度(LeetCode 739・中等)
题目:给定整数数组 temperatures 表示每日温度,返回数组 answer,answer[i] 表示第 i 天之后,下一个更高温度间隔的天数;无更高温度则为 0。
解题思路
单调递减栈(栈内下标对应的温度保持递减):
- 栈中存储数组下标,保证下标对应温度单调递减
- 遍历每日温度:若当前温度 > 栈顶下标对应温度,说明找到了栈顶那天的下一个更高温,计算天数差并弹出栈顶;重复直到栈为空或当前温度不大于栈顶温度,再将当前下标压栈
单调栈执行示意(以 [73,74,75,71,69,72] 为例)
核心 Java 代码
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // 存储数组下标
for (int i = 0; i < n; i++) {
// 当前温度大于栈顶对应温度,弹出计算
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prev = stack.pop();
ans[prev] = i - prev;
}
stack.push(i);
}
return ans;
}💡 考点点睛
- 单调栈模板题,「下一个更大元素」类问题的通用解法
- 时间复杂度 O (n):每个元素最多入栈、出栈各一次
- 同类延伸题:下一个更大元素 I/II、接雨水
🧮 高频题 4:逆波兰表达式求值(LeetCode 150・中等)
题目:给定字符串数组 tokens 表示逆波兰表达式(后缀表达式),计算其值。有效运算符为 + - * /,操作数均为整数。
解题思路
后缀表达式天然适配栈结构:
- 遇到数字,压入栈
- 遇到运算符,弹出栈顶两个数(注意顺序:先弹出的是右操作数,后弹出的是左操作数),运算后将结果压回栈
- 遍历结束后,栈中剩余元素即为最终结果
核心 Java 代码
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
if ("+-*/".contains(token)) {
int b = stack.pop(); // 右操作数
int a = stack.pop(); // 左操作数
switch (token) {
case "+": stack.push(a + b); break;
case "-": stack.push(a - b); break;
case "*": stack.push(a * b); break;
case "/": stack.push(a / b); break;
}
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}💡 考点点睛
- 栈的经典应用场景:表达式求值、编译器语法分析
- 高频易错点:减法、除法的操作数顺序,先弹出的是右侧操作数
📊 高频题 5:柱状图中最大的矩形(LeetCode 84・困难)
题目:给定 n 个非负整数表示柱状图中每个柱子的高度,柱子宽度为 1,求柱状图中能勾勒出的矩形的最大面积。
解题思路
单调递增栈:
- 核心逻辑:对每个柱子,找到「左边第一个比它矮的柱子」和「右边第一个比它矮的柱子」,宽度 = 右下标 - 左下标 - 1,面积 = 高度 × 宽度
- 用单调递增栈存下标,遇到比栈顶小的元素时,栈顶元素的右边界就已确定,左边界为新的栈顶
- 技巧:首尾加 0 作为哨兵,统一边界处理,无需单独判断栈空
核心 Java 代码
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// 首尾加0哨兵,统一边界判断
int[] newHeights = new int[n + 2];
System.arraycopy(heights, 0, newHeights, 1, n);
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0;
for (int i = 0; i < newHeights.length; i++) {
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
int h = newHeights[stack.pop()];
int w = i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}💡 考点点睛
- 单调栈的困难级应用,大厂压轴题高频考点
- 哨兵技巧是面试加分项,能体现代码简洁性和边界处理能力
- 同类延伸题:最大矩形(LeetCode 85)
🎯 面试加分总结
- 栈的适用场景口诀:匹配类、嵌套类、下一个更大 / 更小类、表达式求值、消除类问题,优先考虑栈
- 单调栈通用规律:
- 找下一个更大元素 → 单调递减栈
- 找下一个更小元素 → 单调递增栈
- Java 实现规范:永远用
Deque + ArrayDeque替代Stack类,体现基础扎实度
核心代码 & 技术亮点标注
1. 有效的括号(LeetCode 20)
public boolean isValid(String s) {
// ✅ 技术亮点1:用ArrayDeque替代Stack,无同步锁性能更高,符合Java官方推荐规范
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
// ✅ 技术亮点2:左括号直接压对应右括号,匹配时无需二次映射判断,省去哈希表开销
if (c == '(') stack.push(')');
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
// ✅ 技术亮点3:提前剪枝,栈空或不匹配直接返回,无需遍历剩余字符
else if (stack.isEmpty() || stack.pop() != c) return false;
}
return stack.isEmpty();
}面试可讲亮点:
- 选型规范:摒弃过时的
Stack类,采用Deque + ArrayDeque实现,避免 Vector 同步锁带来的性能损耗,体现 Java 基础扎实度 - 逻辑优化:左括号直接压入对应右括号,匹配阶段直接等值比较,代码简洁且无额外空间开销
- 提前终止:遇到不匹配立即返回,平均场景下时间效率优于完整遍历
2. 最小栈(LeetCode 155)
基础标准版
class MinStack {
Deque<Integer> mainStack;
Deque<Integer> minStack;
public MinStack() {
mainStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
mainStack.push(val);
// ✅ 技术亮点:辅助栈同步压入当前最小值,栈顶永远是全局最小值
minStack.push(minStack.isEmpty() ? val : Math.min(val, minStack.peek()));
}
public void pop() {
// ✅ 技术亮点:双栈严格同步弹出,保证最小值与主栈元素一一对应
mainStack.pop();
minStack.pop();
}
public int top() { return mainStack.peek(); }
public int getMin() { return minStack.peek(); }
}进阶空间优化版(面试加分代码)
// ✅ 技术亮点:重复最小值仅存一次,大幅节省辅助栈空间
public void push(int val) {
mainStack.push(val);
// 仅当新元素小于等于当前最小值时才压入辅助栈
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
int popVal = mainStack.pop();
// 仅当弹出值等于当前最小值时,辅助栈才弹出
if (popVal == minStack.peek()) {
minStack.pop();
}
}面试可讲亮点:
- 设计思想:经典空间换时间,用辅助栈实现 O (1) 时间获取最小值
- 优化思维:重复最小值仅存储一次,极端场景(大量重复元素)下辅助栈空间节省 50% 以上
- 鲁棒性:基础版双栈同进同出,逻辑简单不易出错;优化版按需弹出,体现空间优化意识
3. 每日温度(LeetCode 739)
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
// ✅ 技术亮点1:栈存数组下标而非值,可同时获取温度值和计算间隔天数
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// ✅ 技术亮点2:单调递减栈标准模板,while循环弹出所有可匹配的元素
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prev = stack.pop();
ans[prev] = i - prev;
}
stack.push(i);
}
return ans;
}面试可讲亮点:
- 模板化实现:标准单调递减栈写法,可无缝复用到所有「下一个更大元素」类问题
- 通用性设计:通过下标同时完成值比较和距离计算,无需额外封装对象
- 复杂度优势:每个元素仅入栈、出栈各一次,整体 O (n) 时间,远优于暴力 O (n²)
4. 逆波兰表达式求值(LeetCode 150)
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
// ✅ 技术亮点1:字符串contains快速判断运算符,简化分支逻辑
if ("+-*/".contains(token)) {
// ✅ 技术亮点2:严格遵循「先弹右操作数、后弹左操作数」,规避减法除法顺序错误
int b = stack.pop();
int a = stack.pop();
int res = switch (token) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> a / b;
default -> 0;
};
stack.push(res);
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}面试可讲亮点:
- 结构适配:后缀表达式天然适配栈结构,无需处理运算符优先级,是栈的经典应用场景
- 易错点规避:明确操作数弹出顺序,从根源避免减法、除法的顺序错误
- 代码简洁性:用字符串匹配 + switch 分支,逻辑清晰易读
5. 柱状图中最大的矩形(LeetCode 84)
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// ✅ 技术亮点1:首尾加0哨兵,统一边界处理,无需单独判断栈空情况
int[] newHeights = new int[n + 2];
System.arraycopy(heights, 0, newHeights, 1, n);
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0;
for (int i = 0; i < newHeights.length; i++) {
// ✅ 技术亮点2:单调递增栈,遇到更小元素时确定栈顶元素的右边界
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
int h = newHeights[stack.pop()];
// ✅ 技术亮点3:宽度公式:右边界下标 - 左边界下标 - 1
int w = i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}面试可讲亮点:
- 哨兵技巧:首尾添加高度为 0 的哨兵柱子,彻底规避栈空特判和首尾柱子的边界问题,代码简洁且不易出错
- 核心思想:一次遍历同时找到每个柱子的左右第一个更小边界,将暴力 O (n²) 优化到 O (n)
- 可复用性:宽度计算公式通用,所有「找左右边界求面积」类问题均可套用
🎯 栈类算法通用技术难点 & 解决方案汇总
| 技术难点 | 问题表现 | 解决方案 | 面试加分话术 |
|---|---|---|---|
| 栈实现类选型错误 | 误用java.util.Stack,性能差且继承体系不规范 | 统一使用Deque接口,实现类优先选ArrayDeque;LinkedList可作为备选但内存开销更大 | Stack 是 JDK 早期遗留类,继承自 Vector 自带同步锁,单线程场景下性能远低于 ArrayDeque,官方也早已不推荐使用 |
| 单调栈单调性搞反 | 结果完全错误,找不到正确的边界元素 | 记牢口诀:找「下一个更大元素」用单调递减栈;找「下一个更小元素」用单调递增栈 | 我判断单调性的核心逻辑是:遇到目标值时需要弹出栈内不符合条件的元素,栈内剩余元素自然保持对应单调性 |
| 边界条件遗漏 | 栈空弹出抛异常、首尾元素计算不到结果、数组越界 | 1. 弹出前必须判空;2. 哨兵技巧:数组首尾添加极值元素统一边界;3. 结果数组初始化默认值兜底 | 我处理边界问题常用哨兵技巧,比如柱状图问题首尾加 0,既避免了栈空判断,又保证首尾柱子都能被正确计算 |
| 双栈操作不同步 | 最小栈等设计题中,最小值与主栈元素不对应,结果错乱 | 入栈、出栈操作严格同步;优化版需明确弹出条件,值相等时才触发辅助栈弹出 | 双栈问题最容易踩的坑就是只弹主栈不弹辅助栈,我写的时候会严格保证两个栈的操作成对出现 |
| 表达式求值操作数顺序颠倒 | 减法、除法结果符号或数值完全错误 | 牢记「先弹出的是右操作数,后弹出的是左操作数」,运算时左操作数在前 | 后缀表达式是左操作数在前、右操作数在后,入栈顺序先左后右,所以弹出时先拿到的是右操作数,减法除法一定要注意顺序 |
| 单调栈存值还是存下标混淆 | 无法计算元素间隔、矩形宽度等位置相关结果 | 仅需值比较的场景可存值;涉及位置、距离、宽度计算的场景,一律存下标 | 我一般默认存下标,因为下标既能拿到值,又能计算位置差,通用性更强,只有纯值匹配的场景才直接存值 |
| 相等元素处理不当 | 单调栈死循环、边界计算错误、结果偏差 | 明确相等元素的处理逻辑:找严格更大 / 更小时,相等元素直接压栈;找非严格的则弹出 | 比如每日温度问题,温度相等时不算更高温度,所以相等时直接压栈,保证每个元素的右边界都是严格大于它的第一个元素 |
真实面试模拟
真实面试模拟
面试官:
你好,欢迎来参加今天的 Java 后端开发面试,我是李哥。咱们先不整那些虚的,从最基础也最能看出功底的“栈”开始,可以吧?😊
求职者:
没问题,李哥,栈这一块我准备得还算扎实。
面试官:
行,那你先跟我聊聊,栈这种数据结构最核心的特点是什么?在 Java 里你一般怎么用它?
求职者:
栈的核心就是后进先出,LIFO——像一摞盘子,后放的先取。🥣
Java 里我以前也用过 java.util.Stack,但后来读源码发现它继承 Vector,所有方法都加了重量级同步锁,性能不好。现在规范的做法是用 Deque 接口,配合 ArrayDeque 来当栈用。push、pop、peek 这些方法都有,既高效又符合设计。
面试官:
👍 不错,能说出 Stack 为什么废弃,说明你看过源码。那我再问深一点:ArrayDeque 为什么比 Stack 快?它怎么做到既能当栈又能当队列的?
求职者:
ArrayDeque 底层是一个循环数组,头尾指针可以灵活移动。当栈用的时候只操作头部,入栈就是头指针前移并赋值,出栈就是取头指针值然后后移,都是 O(1) 且没有锁竞争。而 Stack 基于数组但继承 Vector,方法都被 synchronized 修饰,白白多了加锁解锁的开销。
面试官:
很好,基础很扎实。那咱们来上道题热热身:有效的括号(leetcode 20)。给定一个只包含 ()、[]、{} 的字符串,判断它是不是闭合有效的。你先说思路。
求职者:
思路就是用栈。遍历字符,遇到左括号就把对应的右括号压入栈,遇到右括号就检查栈顶是不是它:是就弹出,不是或者栈为空就说明不匹配。最后栈为空才有效。这样省去了判断配对的麻烦,代码更简洁。
面试官:
存期望的右括号,这个小技巧挺聪明。那如果字符串里混入了其他字符呢?比如数字或字母。
求职者:
根据题意可以忽略,或者直接返回 false,看需求。但核心逻辑不变,只处理括号字符。我写一下核心代码吧。
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')');
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else if (stack.isEmpty() || stack.pop() != c) return false;
}
return stack.isEmpty();
}面试官:
嗯,干净。如果我让一个完全不懂的人也能看懂,你能画个图表示这个过程吗?
求职者:
当然。拿输入 "[()]" 举例,扫描过程可以这样可视化:
输入: "[ ( ) ]"
扫描:
[ → 栈: [ ]
( → 栈: [ ], )
) → 遇到 ), 栈顶 ), 匹配弹出 → 栈: [ ]
] → 遇到 ], 栈顶 ], 匹配弹出 → 栈空
✅ 有效面试官:
清晰。时空复杂度多少?
求职者:
时间复杂度 O(n),只遍历一次;空间复杂度最坏 O(n),全是左括号的情况。
面试官:
好,那接着来。逆波兰表达式求值(leetcode 150) 知道吧?给你 ["2","1","+","3","*"],应该输出 9。你写一下,并讲讲注意点。
求职者:
逆波兰式就是后缀表达式,很适合栈。遇到数字压栈,遇到运算符就弹出两个操作数,计算后把结果再压栈。这里最容易错的是操作数顺序:减法和除法,先弹出的是右操作数,后弹出的是左操作数。必须用 int b = stack.pop(); int a = stack.pop(); 再 a - b。
面试官:
对的,很多人在这个顺序上栽跟头。把代码写一下吧。
求职者:
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
switch (t) {
case "+" -> stack.push(stack.pop() + stack.pop());
case "-" -> { int b = stack.pop(), a = stack.pop(); stack.push(a - b); }
case "*" -> stack.push(stack.pop() * stack.pop());
case "/" -> { int b = stack.pop(), a = stack.pop(); stack.push(a / b); }
default -> stack.push(Integer.parseInt(t));
}
}
return stack.pop();
}面试官:
能把这个计算过程用表格展现出来吗?这样更直观。
求职者:
没问题,看这个表 👇
| 步骤 | 当前符号 | 栈状态 (底→顶) | 操作说明 |
|---|---|---|---|
| 1 | 2 | [2] | 数字,压栈 |
| 2 | 1 | [2,1] | 数字,压栈 |
| 3 | + | [3] | 弹 2 和 1,2+1=3 压回 |
| 4 | 3 | [3,3] | 数字,压栈 |
| 5 | * | [9] | 弹 3 和 3,3*3=9 压回,结束 |
面试官:
非常好。那咱们上点难度,来个设计题:最小栈(leetcode 155)。要求实现一个栈,在 O(1) 时间内能获取栈中最小元素,你会怎么设计?🧐
求职者:
典型的空间换时间。我设计两个栈:一个正常栈 dataStack,一个辅助栈 minStack,同步记录每个状态的最小值。push(x) 时,minStack 压入 min(x, minStack.peek());pop 时两个栈同时弹出。这样任何时候 minStack 的栈顶就是当前栈的最小值。
面试官:
思路清晰。那如果面试官追问“能不能省点空间”,你怎么优化?
求职者:
可以有条件压入。只有新压入值 ≤ 当前最小值时,才向 minStack 压入。弹出时,只有当弹出的值等于 minStack 栈顶,才同步弹出 minStack。这样在有很多重复最小值或者递增序列时,辅助栈会小一些。但核心还是 O(1) 时间获取最小值。
面试官:
可以,画个示意图我看看。
求职者:
操作: push(5), push(2), push(3)
dataStack: [5] [5,2] [5,2,3]
minStack: [5] [5,2] [5,2,2] ← 3与顶2比较,存较小值2
getMin() -> 2面试官:
好。那现在换个角度,不用设计模式了,让你用栈实现一个队列(leetcode 232)。你讲讲思路,写一下核心方法。
求职者:
得用两个栈,一个负责入队 in,一个负责出队 out。入队直接压入 in;出队时若 out 为空,就把 in 的所有元素弹出并压入 out,这样就实现了先进先出的逆序,再从 out 弹出栈顶即可。每个元素只会被搬移一次,所以均摊复杂度是 O(1)。
class MyQueue {
Deque<Integer> in = new ArrayDeque<>();
Deque<Integer> out = new ArrayDeque<>();
public void push(int x) { in.push(x); }
public int pop() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.push(in.pop());
}
return out.pop();
}
public int peek() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.push(in.pop());
}
return out.peek();
}
public boolean empty() { return in.isEmpty() && out.isEmpty(); }
}面试官:
嗯,均摊分析说得也到位。看来你对栈的翻转操作理解很深了。最后来一道考察单调栈的题:每日温度(leetcode 739)。给定温度数组 T,返回一个数组,表示对于每一天,距离下一次升温还需要等几天。如果之后没有更高温度,就是 0。你先解释下单调栈在这里怎么用。
求职者:
我维护一个从栈底到栈顶温度递减的下标栈。遍历数组时,如果当前温度高于栈顶下标对应的温度,就说明找到了栈顶那一天的下一个更高温度,计算天数差并出栈,重复这个过程直到栈空或当前温度不再大于栈顶,然后压入当前下标。最后留在栈里的就是后面没有更高温度的,结果就是 0。
面试官:
好,把它写出来,并且也能可视化演示一段温度变化。
求职者:
代码在这:
public int[] dailyTemperatures(int[] T) {
int[] res = new int[T.length];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < T.length; i++) {
while (!stack.isEmpty() && T[i] > T[stack.peek()]) {
int prev = stack.pop();
res[prev] = i - prev;
}
stack.push(i);
}
return res;
}可视化步骤(温度 [73,74,75,71,69,72]):
i=0(73): 栈空,压0 -> [0]
i=1(74): 74>73,弹0,res[0]=1;压1 -> [1]
i=2(75): 75>74,弹1,res[1]=1;压2 -> [2]
i=3(71): 71<75,压3 -> [2,3]
i=4(69): 69<71,压4 -> [2,3,4]
i=5(72): 72>69,弹4,res[4]=1;72>71,弹3,res[3]=2;72<75,压5 -> [2,5]
结果:[1,1,0,2,1,0]面试官:
完美。今天我们聊的栈,从基础的 LIFO,到 JDK 实现的取舍,再到括号匹配、后缀表达式、最小栈、双栈仿队列,最后用单调栈解决 Next Greater 问题,基本把面试中栈的考点全覆盖了。你表现很不错,对每个问题都能抓住本质,还能画图表解释,这在实际工作中是非常好的习惯。👏
面试官李哥:
“刚才的问答你表现得不错。最后我再花几分钟,帮你把今天涉及到的核心代码段、技术亮点,以及技术难点和解决方案捋一遍。这些就是面试官最想听到的关键点,你回去好好消化一下。”
🔑 核心代码 & 技术亮点代码
1. 有效括号匹配(leetcode 20) —— 存期望右括号,消解配对
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')'); // 亮点:直接存期望的右括号
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else if (stack.isEmpty() || stack.pop() != c) return false;
}
return stack.isEmpty();
}亮点:通过压入对应右括号,省去 Map 映射或冗长 if-else 配对判断,代码极简且不易出错。
2. 逆波兰表达式(leetcode 150) —— switch 表达式 + 操作数顺序处理
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
switch (t) {
case "+" -> stack.push(stack.pop() + stack.pop());
case "-" -> {
int b = stack.pop(), a = stack.pop(); // 亮点:显式处理操作数顺序
stack.push(a - b);
}
case "*" -> stack.push(stack.pop() * stack.pop());
case "/" -> {
int b = stack.pop(), a = stack.pop();
stack.push(a / b);
}
default -> stack.push(Integer.valueOf(t));
}
}
return stack.pop();
}亮点:Java 14+ 的箭头 switch 语法简洁;减/除时先弹出的存为 b,后弹出的存为 a,避免计算结果反转。
3. 最小栈(leetcode 155) —— 辅助栈同步记录最小值
class MinStack {
Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> minStack = new ArrayDeque<>();
public void push(int x) {
stack.push(x);
int min = minStack.isEmpty() ? x : Math.min(x, minStack.peek());
minStack.push(min); // 亮点:每一步都保存当前最小值
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() { return stack.peek(); }
public int getMin() { return minStack.peek(); }
}空间优化版亮点(进阶回答时展示):
public void push(int x) {
stack.push(x);
if (minStack.isEmpty() || x <= minStack.peek()) { // 只有 <= 才压入
minStack.push(x);
}
}
public void pop() {
if (stack.pop().equals(minStack.peek())) { // 弹出值等于当前最小值时同步弹出
minStack.pop();
}
}亮点:通过条件压入减少辅助栈空间,同时保证 O(1) 获取最小值。
4. 用栈实现队列(leetcode 232) —— 双栈翻转,均摊 O(1)
class MyQueue {
Deque<Integer> in = new ArrayDeque<>();
Deque<Integer> out = new ArrayDeque<>();
public void push(int x) { in.push(x); }
public int pop() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.push(in.pop()); // 亮点:一次性翻转,均摊 O(1)
}
return out.pop();
}
public int peek() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.push(in.pop());
}
return out.peek();
}
public boolean empty() { return in.isEmpty() && out.isEmpty(); }
}亮点:只有 out 为空时才搬运,保证每个元素最多搬运一次,时间复杂度均摊 O(1)。
5. 单调栈解每日温度(leetcode 739) —— 存下标,遇大则解
public int[] dailyTemperatures(int[] T) {
int[] res = new int[T.length];
Deque<Integer> stack = new ArrayDeque<>(); // 存下标,栈内温度递减
for (int i = 0; i < T.length; i++) {
while (!stack.isEmpty() && T[i] > T[stack.peek()]) {
int prev = stack.pop();
res[prev] = i - prev; // 亮点:下标差即等待天数
}
stack.push(i);
}
return res;
}亮点:单调栈思想一劳永逸解决 Next Greater Element 类问题,用下标存信息,简洁高效。
⚠️ 常见技术难点 & 解决方案
| 难点项 | 易踩坑描述 | 解决方案 & 技巧 |
|---|---|---|
| Java 栈实现选择 | 面试时仍使用 Stack 类,暴露对 JDK 演进不了解 | 统一回答:使用 Deque<Integer> stack = new ArrayDeque<>(),强调去锁、双端灵活性 |
| 出栈操作数顺序 | 减法 / 除法时直接 stack.pop() - stack.pop(),导致顺序反了 | 先用两个变量 b = pop(), a = pop() 接收,再做 a - b / a / b |
| 空栈访问 | 弹出或 peek 前忘记判空,导致 NoSuchElementException | 所有 pop/peek 前必须检查 isEmpty();或者在循环中作为条件(如 while 中取 peek) |
| 辅助栈与主栈不同步 | 最小栈弹出时忘记处理 minStack,导致 getMin 值错误 | push/pop 必须成对操作,或者采用条件同步(弹出值 == min 时弹出 minStack) |
| 双栈实现队列的复杂度分析 | 只说 O(1),说不出均摊依据 | 明确回答:每个元素只在 out 空时从 in 搬运一次,n 次操作总搬运 n 次,均摊 O(1) |
| 单调栈何时用下标何时用值 | 不知道存下标能得到距离信息 | 需要计算“距离”、“间隔”这类信息时,栈内必须存下标,值为单纯的比较条件 |
| 边界条件处理 | 字符串扫描完栈未空(左括号多余),或中途右括号不匹配处理不及时 | 最终检查 return stack.isEmpty();中途不匹配直接 return false |
| 后缀表达式计算忽略负数/多位数 | Integer.parseInt 能处理,但遇到非数字符号判断分支有遗漏 | 用 switch 明确覆盖 + - * /,default 分支做数字解析;注意除法向零截断(Java 默认行为) |
