算法精讲——树(一):DFS 的奇妙探险之旅
算法精讲——树(一):DFS 的奇妙探险之旅🌳🚀
📅 2025 年 03 月 04 日 | 作者:无限大 | 标签:#算法 #树 #DFS
一、开篇小剧场 🎭
想象你是一位在茂密森林 🌲 里探险的冒险家,每次遇到分叉路口时,你都会选择最深的路径一路走到底,直到发现宝藏 💎 或死胡同才返回——这就是我们今天要讲的深度优先搜索(DFS) 的生动写照!
二、DFS 核心原理 🔍
1. 什么是 DFS(深度优先搜索)?
DFS(Depth-First Search)就像探险家的执着精神:
- 策略:不撞南墙不回头,优先探索最深的节点
- 实现方式:递归 / 栈(Stack)
- 时间复杂度:O(n)(每个节点访问一次)
核心要点:DFS 是策略,不同顺序遍历是方法
2. 二叉树 DFS 的三种方式 🤸
遍历结果速查表
遍历类型 | 顺序 | 口诀 | 关键特征 | 场景应用 | 力扣真题 |
---|---|---|---|---|---|
前序 | 根→左→右 1 2 4 5 3 6 | 👑 根 → 左 → 右 | 根节点是第一个访问的结点 | 快速克隆树结构 | 144.前序遍历 |
中序 | 左→根→右 4 2 5 1 3 6 | 🤔 左 → 根 → 右 | 根节点在中间分割左右子树 | 二叉搜索树特性 | 94.中序遍历 |
后序 | 左→右→根 4 5 2 6 3 1 | 🎁 左 → 右 → 根 | 根节点是最后一个访问的结点 | 删除树节点 | 145.后序遍历 |
遍历流程详解
1. 前序遍历(DLR)
访问顺序:根 → 左 → 右步骤分解:
- 访问根节点 1
- 递归遍历左子树(节点 2 的左子树 → 4)
- 递归遍历右子树(节点 2 的右子树 → 5)
- 递归遍历根节点的右子树(节点 3 → 6)
路径图:
1 → 2 → 4 → 5 → 3 → 6
2. 中序遍历(LDR)
访问顺序:左 → 根 → 右步骤分解:
- 递归遍历左子树(节点 2 的左子树 → 4)
- 访问根节点 2
- 递归遍历右子树(节点 2 的右子树 → 5)
- 访问根节点 1
- 递归遍历右子树(节点 3 的左子树为空 → 访问 3 → 访问 6)
路径图:
4 → 2 → 5 → 1 → 3 → 6
3. 后序遍历(LRD)
访问顺序:左 → 右 → 根步骤分解:
- 递归遍历左子树(节点 2 的左子树 → 4)
- 递归遍历右子树(节点 2 的右子树 → 5)
- 访问根节点 2
- 递归遍历右子树(节点 3 的右子树 → 6 → 访问 3)
- 访问根节点 1
路径图:
4 → 5 → 2 → 6 → 3 → 1
三、解题思路具体分析 🔥
1. DFS 问题识别雷达 🕵️
遇到以下特征时优先考虑 DFS:
- 需要遍历所有可能路径
- 问题可分解为子树问题
- 需要回溯操作(如路径记录)
- 求极值/存在性问题
2. 四步拆解法 💡
步骤详解表
步骤 | 操作要点 | 示例问题 |
---|---|---|
1 | 将问题转化为树/图结构 | 路径总和 → 路径遍历 |
2 | 明确当前节点状态 | 当前路径和+剩余目标值 |
3 | 设置递归终止基线 | 叶子节点判断 |
4 | 定义向子节点的状态转移方式 | 选择左/右子树 |
四、通用解题模板 🛠️
递归版模板
public ReturnType dfs(TreeNode node, 附加参数) {
// 🚩1. 终止条件
if (node == null) return baseCase;
if (满足特定条件) return 结果值;
// ✨2. 处理当前节点(前序位置)
处理逻辑;
// 🌳3. 递归子节点
ReturnType left = dfs(node.left, 更新参数);
ReturnType right = dfs(node.right, 更新参数);
// 🎯4. 后序处理(可选)
return 合并结果(left, right);
}
迭代版模板
public ReturnType dfsIterative(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
// 💡注意入栈顺序:前序->右左入栈,中序->特殊处理
if (节点需处理) {
处理逻辑;
}
// 按遍历顺序反向压栈
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
return 结果;
}
模板选择指南
场景 | 推荐模板 | 原因 |
---|---|---|
简单路径问题 | 递归 | 代码简洁直观 |
复杂状态管理 | 迭代 | 避免栈溢出 |
需要回溯操作 | 递归+全局变量 | 方便状态回退 |
严格深度优先 | 迭代 | 显式控制栈操作 |
五、代码实战演练 ⚔️
案例1:路径总和(力扣112)
// ✅递归模板的完美实践
public boolean hasPathSum(TreeNode root, int targetSum) {
// 🚩终止条件1:空节点
if (root == null) return false;
// 🚩终止条件2:叶子节点
if (root.left == null && root.right == null) {
return targetSum == root.val; // ✨结果判断
}
// 🌳递归子节点(更新剩余目标值)
return hasPathSum(root.left, targetSum - root.val)
|| hasPathSum(root.right, targetSum - root.val);
}
案例2:二叉树的中序遍历(力扣94)
// ✅迭代模板的典型应用
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
// 🎮显式控制遍历流程
while (curr != null || !stack.isEmpty()) {
// 🌳左探针直达最深处
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
res.add(curr.val); // ✨访问节点
curr = curr.right; // ➡️转向右子树
}
return res;
}
图例分析
六、避坑指南 ⚠️
常见错误对照表
错误现象 | 错误原因 | 解决方案 |
---|---|---|
栈溢出(StackOverflow) | 递归深度超过系统限制 | 改用迭代+手动维护栈 |
路径结果重复 | 未及时回溯状态 | 添加 path.removeLast() |
空指针异常 | 未判断节点是否为null | 添加 if(node==null) 检查 |
死循环 | 图中未标记已访问节点 | 使用 visited 集合记录 |
七、知识宇宙扩展 🪐
DFS 变种应用表
变种类型 | 应用场景 | 经典题目 |
---|---|---|
记忆化 DFS | 重叠子问题优化 | 70.爬楼梯 |
双向 DFS | 超大搜索空间优化 | 127.单词接龙 |
剪枝 DFS | 组合类问题优化 | 39.组合总和 |
八、今日小结 📌
- 🧭 DFS 是纵向搜索的典型代表
- 🛠 掌握递归与迭代两种实现方式
- 🎮 通过树类问题理解 DFS 的精髓
九、下期剧透 🔮
明日主题 :BFS 层序遍历的魔法——像水波纹一样扫描整棵树!
亮点预告 :
- 🌀 队列(Queue)的妙用技巧
- 🎯 最短路径问题的破解之道
- 💡 双向 BFS 优化秘籍
🌟 课后作业 :用 DFS 实现二叉树的镜像翻转,把你的代码截图发到评论区吧!