🔍常见高频算法题 Java 解法实战精讲(2):堆栈与递归
🧠写在前面:堆栈与递归题型为何频繁出现在面试中?
堆栈(Stack)与递归(Recursion)类题目属于经典的算法考察方向,原因如下:
-
✅ 数据结构基础扎实度测试:考查是否真正理解"先进后出"的栈结构。
-
✅ 代码逻辑清晰与抽象能力:递归常被用于处理树、图等结构,能快速验证抽象思维。
-
✅ Java 线程控制/执行顺序的理解:堆栈和线程调度关系紧密,是并发考点基础。
-
✅ 模板化解题能力评估:是否掌握栈操作与 DFS/BFS 的通用套路,体现工程实践力。
文章目录
- [🔍常见高频算法题 Java 解法实战精讲(2):堆栈与递归](#🔍常见高频算法题 Java 解法实战精讲(2):堆栈与递归)
- 一、堆栈与递归的核心地位
-
- [💡 堆栈与递归的关系](#💡 堆栈与递归的关系)
- [⚠️ 面试高频原因](#⚠️ 面试高频原因)
- 二、堆栈类经典题
-
- [💡 2.1 有效的括号(Valid Parentheses)](#💡 2.1 有效的括号(Valid Parentheses))
- [💡 2.2 逆波兰表达式求值](#💡 2.2 逆波兰表达式求值)
- 三、DFS与BFS模板实战
-
- [💡 3.1 二叉树的最大深度(DFS模板)](#💡 3.1 二叉树的最大深度(DFS模板))
- [💡 3.2 图的最短路径(BFS模板)](#💡 3.2 图的最短路径(BFS模板))
- 四、多线程算法题
-
- [💡 4.1 交替打印FooBar](#💡 4.1 交替打印FooBar)
- [💡 4.2 按序打印ABC](#💡 4.2 按序打印ABC)
- 五、高频面试套路总结
-
- [💡 堆栈类题目套路](#💡 堆栈类题目套路)
- [🔄 搜索算法模板](#🔄 搜索算法模板)
- [⚡️ 多线程解题框架](#⚡️ 多线程解题框架)
- [🛡️ 面试避坑指南](#🛡️ 面试避坑指南)
一、堆栈与递归的核心地位
💡 堆栈与递归的关系
递归 函数调用栈 栈帧 局部变量 返回地址 参数传递
⚠️ 面试高频原因
原因 | 考察点 | 出现频率 |
---|---|---|
基础能力 | 函数调用机制 | 90% |
思维转换 | 递归转迭代 | 75% |
系统设计 | 栈溢出防护 | 60% |
复杂问题 | 树/图遍历 | 85% |
二、堆栈类经典题
💡 2.1 有效的括号(Valid Parentheses)
题目描述:
给定只包含'(', ')', '{', '}', '[', ']'的字符串,判断括号是否有效
Java解法:
java
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
Map<Character, Character> map = Map.of(')', '(', '}', '{', ']', '[');
for (char c : s.toCharArray()) {
if (map.containsValue(c)) {
stack.push(c);
} else if (stack.isEmpty() || stack.pop() != map.get(c)) {
return false;
}
}
return stack.isEmpty();
}
复杂度分析:
- 时间复杂度:O(n) 单次遍历
- 空间复杂度:O(n) 栈空间
常见陷阱:
java
// 错误:未处理空栈情况
if (stack.pop() != map.get(c)) // 当栈空时抛出异常
// 正确:先检查栈空
if (stack.isEmpty() || stack.pop() != map.get(c))
💡 2.2 逆波兰表达式求值
题目描述:
根据逆波兰表示法(后缀表达式)求值,如["2","1","+","3","*"]→ (2+1)*3=9
Java解法:
java
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : tokens) {
switch (token) {
case "+":
stack.push(stack.pop() + stack.pop());
break;
case "-":
int b = stack.pop(), a = stack.pop();
stack.push(a - b);
break;
case "*":
stack.push(stack.pop() * stack.pop());
break;
case "/":
int divisor = stack.pop(), dividend = stack.pop();
stack.push(dividend / divisor);
break;
default:
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}
操作流程:
2 入栈 1 入栈 + 弹出1和2 计算2+1=3入栈 3 入栈 * 弹出3和3 计算3 * 3=9入栈
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
三、DFS与BFS模板实战
💡 3.1 二叉树的最大深度(DFS模板)
题目描述:
求二叉树的最大深度
递归解法:
java
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
迭代解法:
java
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Deque<TreeNode> stack = new ArrayDeque<>();
Deque<Integer> depths = new ArrayDeque<>();
stack.push(root);
depths.push(1);
int max = 0;
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
int depth = depths.pop();
max = Math.max(max, depth);
if (node.right != null) {
stack.push(node.right);
depths.push(depth + 1);
}
if (node.left != null) {
stack.push(node.left);
depths.push(depth + 1);
}
}
return max;
}
DFS模板总结:
java
// 递归模板
void dfs(Node node) {
if (终止条件) return;
for (Node child : node.children) {
dfs(child);
}
}
// 迭代模板
void dfs(Node root) {
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node node = stack.pop();
for (Node child : node.children) {
stack.push(child);
}
}
}
💡 3.2 图的最短路径(BFS模板)
题目描述:
在无权图中求从起点到终点的最短路径
Java解法:
java
public int shortestPath(int[][] graph, int start, int end) {
Queue<Integer> queue = new LinkedList<>();
boolean[] visited = new boolean[graph.length];
int[] distance = new int[graph.length];
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int node = queue.poll();
if (node == end) return distance[node];
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
distance[neighbor] = distance[node] + 1;
queue.offer(neighbor);
}
}
}
return -1; // 不可达
}
BFS模板总结:
java
void bfs(Node start) {
Queue<Node> queue = new LinkedList<>();
Set<Node> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
Node node = queue.poll();
for (Node neighbor : node.neighbors) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
复杂度对比:
算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
---|---|---|---|
DFS | O(V+E) | O(h) | 深度优先/路径存在 |
BFS | O(V+E) | O(w) | 最短路径/层级遍历 |
四、多线程算法题
💡 4.1 交替打印FooBar
题目描述:
两个线程交替打印"foo"和"bar"各n次
Semaphore解法:
java
class FooBar {
private int n;
private Semaphore fooSem = new Semaphore(1);
private Semaphore barSem = new Semaphore(0);
public FooBar(int n) { this.n = n; }
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
fooSem.acquire();
printFoo.run();
barSem.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
barSem.acquire();
printBar.run();
fooSem.release();
}
}
}
💡 4.2 按序打印ABC
题目描述:
三个线程按顺序循环打印"A""B""C"
ReentrantLock解法:
java
class ABCPrinter {
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
private int state = 0; // 0:A, 1:B, 2:C
public void printA() throws InterruptedException {
lock.lock();
try {
while (state != 0) conditionA.await();
System.out.print("A");
state = 1;
conditionB.signal();
} finally {
lock.unlock();
}
}
public void printB() throws InterruptedException {
lock.lock();
try {
while (state != 1) conditionB.await();
System.out.print("B");
state = 2;
conditionC.signal();
} finally {
lock.unlock();
}
}
public void printC() throws InterruptedException {
lock.lock();
try {
while (state != 2) conditionC.await();
System.out.print("C");
state = 0;
conditionA.signal();
} finally {
lock.unlock();
}
}
}
多线程解题技巧:
多线程同步 信号量 锁+条件变量 volatile+自旋 阻塞队列 简单同步 精细控制 低延迟 解耦生产消费
五、高频面试套路总结
💡 堆栈类题目套路
-
括号匹配:
- 使用栈存储左括号
- 遇到右括号时弹出匹配
- 最后检查栈是否为空
-
表达式求值:
- 中缀转后缀(调度场算法)
- 后缀表达式求值(操作数栈)
- 处理运算符优先级
-
单调栈:
- 解决"下一个更大元素"问题
- 保持栈内元素单调性
- 时间复杂度O(n)
🔄 搜索算法模板
java
// DFS递归模板
void dfs(Node node, State state) {
if (终止条件) {
更新结果;
return;
}
for (选择 : 当前选择列表) {
做选择;
dfs(下一节点, 新状态);
撤销选择;
}
}
// BFS模板
void bfs(Node start) {
Queue<Node> queue = new LinkedList<>();
Set<Node> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
Node node = queue.poll();
for (Node neighbor : node.neighbors) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
⚡️ 多线程解题框架
java
| 问题类型 | 推荐方案 | 关键点 |
|---------|---------|-------|
| **顺序控制** | 信号量 | acquire/release顺序 |
| **交替执行** | 条件变量 | await/signal精准通知 |
| **并行计算** | Future/CompletableFuture | 异步结果聚合 |
| **资源池** | Semaphore | 控制并发数 |
🛡️ 面试避坑指南
-
递归陷阱:
- 栈溢出:限制递归深度
- 重复计算:使用记忆化
-
DFS/BFS选择:
- DFS:路径存在性/所有解
- BFS:最短路径/最少步数
-
多线程安全:
- 避免死锁:固定锁顺序
- 防止饥饿:公平锁/超时
- 可见性:volatile/原子类
模板是基础 :掌握DFS/BFS标准写法
递归转迭代 :避免栈溢出风险
线程安全第一 :多线程优先考虑正确性
记住:算法面试不是考记忆,而是考思维