多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

刷穿 LeetCode 113:路径总和 II —— 二叉树 DFS 遍历与回溯收集全路径的实战指南

刷穿 LeetCode 113:路径总和 II —— 二叉树 DFS 遍历与回溯收集全路径的实战指南 教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载导读本文讲解 LeetCode113. 路径总和 II中等的完整解法该题是「二叉树 DFS」的经典入门题在一棵二叉树中找出所有从根节点到叶子节点、路径上节点值之和恰好等于给定目标值targetSum的路径。全文以仓库 LeetCode/111-120/113. 路径总和 II中等.md 的 DFS 题解为主体并结合仓库中 剑指 Offer 34. 二叉树中和为某一值的路径、437. 路径总和 III 等姊妹题做横向对比。读完本文你将掌握「DFS 携带状态递归遍历二叉树」「回溯维护路径列表」「叶子节点判定」「答案拷贝时机」等一整套可复用的二叉树路径收集模板。一、题目解读与边界条件1.1 问题定义给你二叉树的根节点root和一个整数目标和targetSum找出所有从根节点到叶子节点、路径总和等于给定目标和的路径。两个关键约束决定了本题的解题方向路径必须从根节点出发这与 437. 路径总和 III路径可以不从根开始、不必到叶子结束且只统计条数截然不同路径必须在叶子节点结束叶子节点指没有子节点的节点左右孩子均为null要求返回的是所有满足条件的完整路径列表而非简单计数因此需要显式地维护路径。1.2 示例验证示例 1 输入root [5,4,8,11,null,13,4,7,2,null,null,5,1]、targetSum 22输出[[5,4,11,2],[5,8,4,5]]路径5 → 4 → 11 → 2和为5 4 11 2 22且2是叶子节点路径5 → 8 → 4 → 5和为5 8 4 5 22且5是叶子节点注意5 → 4 → 11 → 7虽然和也是22但7不是叶子节点因此不计入答案。示例 2 输入root [1,2,3]、targetSum 5没有从根到叶子的路径和为 5输出[]。示例 3 输入root [1,2]、targetSum 01 2 3 ≠ 0同样输出[]。1.3 数据范围提示树中节点总数在范围[0, 5000]内——节点可以为 0空树代码必须处理root null的情况-1000 Node.val 1000节点值可为负数这意味着不能仅凭「当前和已经超过 targetSum」就提前剪枝必须完整遍历到叶子节点-1000 targetSum 1000。节点值允许为负这一点很容易被忽略若所有值为正累计和超过目标即可剪枝而本题负数存在任何「和大于目标就返回」的优化都是错误的。二、核心解法树的遍历 DFS回溯2.1 算法思路定义DFS函数void dfs(TreeNode root, int cur, int t, ListInteger path)其中参数含义root当前遍历到的节点cur当前路径的累计总和t目标总和targetSumpath当前路径方案从根到当前节点的节点值列表递归逻辑分两种情况到达叶子节点root.left null root.right null此时整条路径已经确定直接比较cur与t是否相等相等则将path的副本加入答案非叶子节点将其非空的孩子节点依次作为下一层递归的入口每进入一个孩子先把孩子值加入path递归返回后再回退path.remove(path.size() - 1)保证path始终只保存「从根到当前节点」这一条路径这就是典型的回溯操作。为什么必须拷贝path再放入答案因为递归返回后path会被后续回溯操作继续修改移除元素。如果直接把path引用加入ans最终答案里的所有路径都会指向同一个不断变化的列表。Java 中new ArrayList(path)完成一次值拷贝是本题最容易踩的坑之一。2.2 参考代码Javaclass Solution { ListListInteger ans new ArrayList(); public ListListInteger pathSum(TreeNode root, int t) { if (root ! null) { dfs(root, root.val, t, new ArrayList() {{ add(root.val); }}); } return ans; } void dfs(TreeNode root, int cur, int t, ListInteger path) { if (root.left null root.right null) { if (cur t) ans.add(new ArrayList(path)); return; } ListTreeNode list new ArrayList(); if (root.left ! null) list.add(root.left); if (root.right ! null) list.add(root.right); for (TreeNode child : list) { path.add(child.val); dfs(child, cur child.val, t, path); path.remove(path.size() - 1); } } }代码要点拆解入口处先判空root ! null时才发起首次 DFS并把根节点值作为path的初始元素、cur的初始值叶子判定在进入子节点之前完成只有当root.left、root.right均为null时才结算避免把「半截路径」误判为合法路径回溯与递归成对出现path.add(child.val)与path.remove(path.size() - 1)严格配对保证路径状态在兄弟分支之间互不污染。2.3 复杂度分析时间复杂度O(n)——每个节点恰好被访问一次n为节点总数将合法路径拷贝进答案的额外开销在最坏情况下所有根到叶路径均合法累计为O(n × h)h为树高空间复杂度O(n)——path列表最多存储一条完整路径长度为树高递归调用栈深度同样为树高h最坏情况下链表状树h n。三、回溯维护路径的关键细节3.1 为什么必须「先判叶子再递归孩子」在dfs开头直接判断cur t是不够的——那会把非叶子节点的路径也加入答案。必须同时满足「到达叶子」与「总和等于目标」两个条件。这正是题目「路径在叶子节点结束」的语义要求。3.2 回溯的两种写法对比仓库 剑指 Offer 34. 二叉树中和为某一值的路径 给出了另一种更简洁的 DFS 写法——先加入、后判断、再回退void dfs(TreeNode root, int cur, ListInteger list) { if (root null) return; list.add(root.val); if (cur root.val t root.left null root.right null) { ans.add(new ArrayList(list)); } dfs(root.left, cur root.val, list); dfs(root.right, cur root.val, list); list.remove(list.size() - 1); // 回溯 }与主文档 113 题解 的写法对比维度113 题解写法剑指 Offer 34 写法空节点处理入口判空 收集孩子再遍历递归开头if (root null) return兜底累计和时机进入孩子前手动cur child.val每层用cur root.val传参回溯位置path.add/path.remove成对出现统一在函数末尾list.remove(list.size() - 1)适用场景根节点已入队、显式遍历孩子列表更通用的「先入队 → 判断 → 递归 → 回退」模板两种写法本质相同进入下一层前状态扩展返回后状态还原。推荐掌握剑指 Offer 34 的通用模板因为它对空指针更鲁棒且把回溯收敛到函数末尾单一位置不容易漏写。3.3 对称问题437. 路径总和 III 的对照同为「路径总和」家族437. 路径总和 III 允许路径从任意节点出发、在任意节点结束只统计条数。其 DFS 递归中不需要维护path列表只累加和并计数若要升级到 O(n) 解法则引入「前缀和 哈希表」并在遍历完左右子树后回溯删除当前前缀和防止统计到跨越两个方向的路径。三题横向对比可快速建立「二叉树路径问题」的解法坐标系题目路径起点/终点输出核心技巧113. 路径总和 II根 → 叶子所有路径DFS 回溯 路径拷贝剑指 Offer 34根 → 叶子所有路径DFS / BFS 双解437. 路径总和 III任意 → 任意向下条数双重 DFS 或 前缀和 哈希四、同一题目的 BFS 变体路径拷贝的成本虽然本题的主流解法是 DFS但仓库姊妹题 剑指 Offer 34 还提供了一套 BFS 解法可以帮助理解「DFS 回溯」的相对优势BFS 需要封装一个结构体Node保存三个字段原始树节点node、到达该节点的路径list、路径总和tot由于 BFS 按层出队相邻出队的节点不一定在同一条路径上无法像 DFS 那样共享并回退同一个path因此每次扩展新节点时都必须对路径做一次拷贝new ArrayList(_list)最坏情况下所有路径均为合法路径时间与空间复杂度均为O(n × h)。对比可知DFS 因为「一条路走到黑」的特性天然适合共享路径列表 回溯复用BFS 则每次入队都要拷贝路径内存峰值更高。这就是记录从根到当前节点的完整路径类问题优先选择 DFS 的原因。五、从题解到模板可复用的二叉树路径收集范式结合 113 题解 与仓库内 Index/DFS.md、Index/二叉树.md 索引中的大量 DFS/二叉树题目可以提炼出「二叉树根到叶路径收集」的通用模板void dfs(TreeNode root, ...状态参数, List... path) { // 1. 空节点兜底可选取决于入口写法 // 2. 状态扩展把当前节点加入 path、更新累计状态 // 3. 叶子结算若为叶子且满足条件拷贝 path 加入答案 // 4. 递归左右孩子传入更新后的状态 // 5. 回溯把当前节点从 path 移除状态回退 }该模板可无缝迁移到仓库中收录的同类题目例如从根到叶的二进制数之和同样是根到叶的路径问题只是把「求和」换成「按二进制位累加」129. 求根节点到叶节点数字之和 一类的题目均可套用「边遍历边维护状态、到叶子结算」的框架。掌握这一范式后遇到「收集所有满足条件的根到叶路径」「统计根到叶路径的某种属性」类问题时只需要修改状态参数与叶子判定条件即可。六、易错点自查清单忘记处理空树root null时直接返回空列表不要调用dfs导致空指针路径拷贝时机错误加入答案时必须new ArrayList(path)否则所有答案共享同一列表最终全部被回溯清空漏写回溯递归返回后忘记path.remove(...)会导致兄弟分支的路径互相叠加结果错误且难以排查在非叶子节点结算只判断cur t而不判断root.left null root.right null会把不完整的路径混入答案误用「和超过目标即剪枝」本题节点值可为负-1000 Node.val 1000当前和超过目标不代表后续路径不合法不能提前返回。七、总结LeetCode 113 路径总和 II 是「二叉树遍历 回溯」的经典题目通过 DFS 从根节点出发携带cur当前路径和与path当前路径列表两个状态递归下降仅在叶子节点处校验cur targetSum并拷贝路径入答案递归返回时回溯还原路径。原题解LeetCode/111-120/113. 路径总和 II中等.md给出的 Java 实现时间复杂度 O(n)、空间复杂度 O(n)。与仓库中 剑指 Offer 34、437. 路径总和 III 对照阅读可以一次吃透「根到叶路径收集」「任意向下路径计数」两大二叉树路径问题并沉淀出一套可复用的 DFS 回溯代码模板。赞分享教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载相关推荐LeetCode-Book 精讲路径总和 IILeetCode 113——先序遍历 回溯记录根到叶路径的完整攻略LeetCode Book 精讲路径总和 IILeetCode 113——先序遍历 回溯记录根到叶路径的完整攻略 导读 《路径总和 II》是二叉树路径示例工程437. 路径总和 III 题解二叉树上的前缀和与 DFS 回溯LeetCode 解题之路437. 路径总和 III 题解二叉树上的前缀和与 DFS 回溯LeetCode 解题之路 导读 本题是 LeetCode 437「路径总和 III」的完文档教程知识库LeetCode 113 路径总和 II用回溯通用写法枚举所有根到叶路径LeetCode 113 路径总和 II用回溯通用写法枚举所有根到叶路径 本篇以 LeetCode 113「路径总和 II」为核心讲解如何把一道树上求集合文档教程知识库上一篇GitHub_Trending/we/WeChatMsg依赖冲突解决版本兼容终极技巧下一篇SnakeAI实战指南从零开始配置环境和运行智能代理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表