《常见高频算法题 Java 解法实战精讲(2):堆栈与递归》
🔍常见高频算法题 Java 解法实战精讲(2):堆栈与递归
🧠写在前面:堆栈与递归题型为何频繁出现在面试中?
堆栈(Stack)与递归(Recursion)类题目属于经典的算法考察方向,原因如下:
-
✅ 数据结构基础扎实度测试:考查是否真正理解“先进后出”的栈结构。
-
✅ 代码逻辑清晰与抽象能力:递归常被用于处理树、图等结构,能快速验证抽象思维。
-
✅ Java 线程控制/执行顺序的理解:堆栈和线程调度关系紧密,是并发考点基础。
-
✅ 模板化解题能力评估:是否掌握栈操作与 DFS/BFS 的通用套路,体现工程实践力。
文章目录
- 🔍常见高频算法题 Java 解法实战精讲(2):堆栈与递归
- 🧠写在前面:堆栈与递归题型为何频繁出现在面试中?
- 一、堆栈与递归的核心地位
- 💡 堆栈与递归的关系
- ⚠️ 面试高频原因
- 二、堆栈类经典题
- 💡 2.1 有效的括号(Valid Parentheses)
- 💡 2.2 逆波兰表达式求值
- 三、DFS与BFS模板实战
- 💡 3.1 二叉树的最大深度(DFS模板)
- 💡 3.2 图的最短路径(BFS模板)
- 四、多线程算法题
- 💡 4.1 交替打印FooBar
- 💡 4.2 按序打印ABC
- 五、高频面试套路总结
- 💡 堆栈类题目套路
- 🔄 搜索算法模板
- ⚡️ 多线程解题框架
- 🛡️ 面试避坑指南
一、堆栈与递归的核心地位
💡 堆栈与递归的关系
⚠️ 面试高频原因
原因 | 考察点 | 出现频率 |
---|---|---|
基础能力 | 函数调用机制 | 90% |
思维转换 | 递归转迭代 | 75% |
系统设计 | 栈溢出防护 | 60% |
复杂问题 | 树/图遍历 | 85% |
二、堆栈类经典题
💡 2.1 有效的括号(Valid Parentheses)
题目描述:
给定只包含’(', ‘)’, ‘{’, ‘}’, ‘[’, ']'的字符串,判断括号是否有效
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) 栈空间
常见陷阱:
// 错误:未处理空栈情况
if (stack.pop() != map.get(c)) // 当栈空时抛出异常// 正确:先检查栈空
if (stack.isEmpty() || stack.pop() != map.get(c))
💡 2.2 逆波兰表达式求值
题目描述:
根据逆波兰表示法(后缀表达式)求值,如[“2”,“1”,“+”,“3”,“*”]→ (2+1)*3=9
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();
}
操作流程:
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
三、DFS与BFS模板实战
💡 3.1 二叉树的最大深度(DFS模板)
题目描述:
求二叉树的最大深度
递归解法:
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;
}
迭代解法:
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模板总结:
// 递归模板
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解法:
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模板总结:
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解法:
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解法:
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:Cpublic 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();}}
}
多线程解题技巧:
五、高频面试套路总结
💡 堆栈类题目套路
-
括号匹配:
- 使用栈存储左括号
- 遇到右括号时弹出匹配
- 最后检查栈是否为空
-
表达式求值:
- 中缀转后缀(调度场算法)
- 后缀表达式求值(操作数栈)
- 处理运算符优先级
-
单调栈:
- 解决"下一个更大元素"问题
- 保持栈内元素单调性
- 时间复杂度O(n)
🔄 搜索算法模板
// 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);}}}
}
⚡️ 多线程解题框架
| 问题类型 | 推荐方案 | 关键点 |
|---------|---------|-------|
| **顺序控制** | 信号量 | acquire/release顺序 |
| **交替执行** | 条件变量 | await/signal精准通知 |
| **并行计算** | Future/CompletableFuture | 异步结果聚合 |
| **资源池** | Semaphore | 控制并发数 |
🛡️ 面试避坑指南
-
递归陷阱:
- 栈溢出:限制递归深度
- 重复计算:使用记忆化
-
DFS/BFS选择:
- DFS:路径存在性/所有解
- BFS:最短路径/最少步数
-
多线程安全:
- 避免死锁:固定锁顺序
- 防止饥饿:公平锁/超时
- 可见性:volatile/原子类
模板是基础:掌握DFS/BFS标准写法
递归转迭代:避免栈溢出风险
线程安全第一:多线程优先考虑正确性
记住:算法面试不是考记忆,而是考思维