【华为机试】113. 路径总和 II
文章目录
- 113. 路径总和 II
- 题目描述
- 示例 1:
- 示例 2:
- 示例 3:
- 提示:
- 解题思路
- 算法分析
- 核心思想
- 关键要素
- 算法对比
- DFS递归回溯流程
- 回溯机制详解
- 完整搜索过程
- BFS层序遍历方法
- 复杂度分析
- 时间复杂度
- 空间复杂度
- 关键实现技巧
- 1. 路径状态管理
- 2. 引用传递+手动回溯
- 3. BFS迭代实现
- 边界情况处理
- 1. 空树处理
- 2. 负数处理
- 3. 路径复制
- 算法优化策略
- 1. 空间优化
- 2. 时间优化
- 3. 内存优化
- 实际应用场景
- 测试用例设计
- 基础测试
- 边界测试
- 性能测试
- 实战技巧总结
- 核心洞察
- 完整题解代码
113. 路径总和 II
题目描述
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例 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]]
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:
输入:root = [1,2], targetSum = 0
输出:[]
提示:
- 树中节点总数在范围 [0, 5000] 内
- -1000 <= Node.val <= 1000
- -1000 <= targetSum <= 1000
解题思路
算法分析
这是一道经典的二叉树路径搜索问题,需要找到所有从根节点到叶子节点且路径和等于目标值的路径。核心思想是深度优先搜索(DFS) + 回溯算法。
核心思想
- DFS遍历:从根节点开始,深度优先遍历所有可能路径
- 路径记录:在遍历过程中记录当前路径上的所有节点
- 条件判断:到达叶子节点时检查路径和是否等于目标值
- 回溯处理:遍历完一个分支后,回溯到上一层继续探索
关键要素
- 叶子节点判断:
node.Left == nil && node.Right == nil
- 路径和计算:累计当前路径上所有节点的值
- 结果收集:满足条件的路径加入结果集
- 回溯机制:确保路径状态的正确维护
算法对比
算法 | 时间复杂度 | 空间复杂度 | 特点 |
---|---|---|---|
DFS递归回溯 | O(N) | O(H) | 经典解法,代码简洁 |
DFS迭代栈 | O(N) | O(H) | 显式栈,空间可控 |
BFS层序遍历 | O(N) | O(W) | 层次遍历,适合宽树 |
Morris遍历 | O(N) | O(1) | 空间最优,实现复杂 |
注:N为节点数,H为树高,W为树的最大宽度
DFS递归回溯流程
graph TDA[开始DFS: root, targetSum] --> B{当前节点为空?}B -->|是| C[返回,路径无效]B -->|否| D[将当前节点值加入路径]D --> E[更新剩余目标和: targetSum - node.val]E --> F{是否为叶子节点?}F -->|是| G{剩余目标和 == 0?}F -->|否| H[递归遍历左子树]G -->|是| I[将当前路径加入结果集]G -->|否| J[路径不符合,忽略]H --> K[递归遍历右子树]I --> L[回溯: 移除当前节点]J --> LK --> LL --> M[返回上一层]
回溯机制详解
graph TDA[进入节点] --> B[path.add(node.val)]B --> C[递归处理子节点]C --> D{处理完所有子节点?}D -->|否| E[继续处理下一个子节点]D -->|是| F[path.remove(最后一个元素)]F --> G[返回上一层]E --> C
完整搜索过程
graph TDA[root=5, target=22, path=[]] --> B[path=[5], remaining=17]B --> C[左子树: 4]B --> D[右子树: 8]C --> E[path=[5,4], remaining=13]E --> F[左子树: 11]F --> G[path=[5,4,11], remaining=2]G --> H[左子树: 7]G --> I[右子树: 2]H --> J[path=[5,4,11,7], remaining=-5]J --> K[叶子节点,和不匹配]I --> L[path=[5,4,11,2], remaining=0]L --> M[叶子节点,和匹配! 收集路径]D --> N[path=[5,8], remaining=14]N --> O[左子树: 13]N --> P[右子树: 4]P --> Q[path=[5,8,4], remaining=10]Q --> R[左子树: 5]Q --> S[右子树: 1]R --> T[path=[5,8,4,5], remaining=5]T --> U[叶子节点,但和不为0]subgraph 结果收集V[收集到的路径:] W[[5,4,11,2]]X[[5,8,4,5]]end
BFS层序遍历方法
graph TDA[初始化队列: node, path, sum] --> B[队列非空?]B -->|否| C[遍历结束]B -->|是| D[出队: 当前节点、路径、累计和]D --> E{是否为叶子节点?}E -->|是| F{累计和 == 目标和?}E -->|否| G[处理左右子节点]F -->|是| H[添加路径到结果]F -->|否| I[忽略此路径]G --> J[左子节点入队: newPath+left, sum+left.val]G --> K[右子节点入队: newPath+right, sum+right.val]H --> L[继续处理下一个节点]I --> LJ --> LK --> LL --> B
复杂度分析
时间复杂度
- 访问节点:O(N),每个节点访问一次
- 路径复制:O(H×K),H为树高,K为有效路径数
- 总体时间:O(N + H×K),通常简化为O(N)
空间复杂度
- 递归栈:O(H),最大递归深度
- 路径存储:O(H),当前路径最大长度
- 结果存储:O(H×K),K个路径,每个最长H
- 总体空间:O(H + H×K) = O(H×K)
关键实现技巧
1. 路径状态管理
// 方法1: 值传递(自动回溯)
func dfs(node *TreeNode, targetSum int, path []int, result *[][]int) {if node == nil {return}// 添加当前节点到路径(新切片)newPath := append(path, node.Val)if node.Left == nil && node.Right == nil && targetSum == node.Val {*result = append(*result, newPath)return}// 递归时传递新路径,无需手动回溯dfs(node.Left, targetSum-node.Val, newPath, result)dfs(node.Right, targetSum-node.Val, newPath, result)
}
2. 引用传递+手动回溯
// 方法2: 引用传递(手动回溯)
func dfs(node *TreeNode, targetSum int, path *[]int, result *[][]int) {if node == nil {return}// 添加当前节点*path = append(*path, node.Val)if node.Left == nil && node.Right == nil && targetSum == node.Val {// 复制路径到结果pathCopy := make([]int, len(*path))copy(pathCopy, *path)*result = append(*result, pathCopy)} else {// 递归处理子节点dfs(node.Left, targetSum-node.Val, path, result)dfs(node.Right, targetSum-node.Val, path, result)}// 回溯:移除当前节点*path = (*path)[:len(*path)-1]
}
3. BFS迭代实现
type PathNode struct {node *TreeNodepath []intsum int
}func pathSumBFS(root *TreeNode, targetSum int) [][]int {if root == nil {return [][]int{}}var result [][]intqueue := []*PathNode{{root, []int{root.Val}, root.Val}}for len(queue) > 0 {current := queue[0]queue = queue[1:]if current.node.Left == nil && current.node.Right == nil {if current.sum == targetSum {result = append(result, current.path)}continue}if current.node.Left != nil {leftPath := append([]int{}, current.path...)leftPath = append(leftPath, current.node.Left.Val)queue = append(queue, &PathNode{current.node.Left, leftPath, current.sum + current.node.Left.Val,})}if current.node.Right != nil {rightPath := append([]int{}, current.path...)rightPath = append(rightPath, current.node.Right.Val)queue = append(queue, &PathNode{current.node.Right, rightPath, current.sum + current.node.Right.Val,})}}return result
}
边界情况处理
1. 空树处理
- 根节点为nil:返回空结果
- 单节点树:检查节点值是否等于目标和
2. 负数处理
- 节点值可能为负数
- 目标和可能为负数
- 路径和的计算需要考虑正负抵消
3. 路径复制
- 确保结果中的路径是独立的副本
- 避免引用同一个切片导致的数据污染
算法优化策略
1. 空间优化
- 使用值传递避免手动回溯
- 预估结果集大小,减少切片扩容
- 路径长度预分配
2. 时间优化
- 提前剪枝:如果当前节点值已经超过剩余目标和(所有后续节点都为正数的情况)
- 路径长度限制:如果路径长度超过合理范围
3. 内存优化
- 复用临时切片
- 及时释放不再使用的路径
实际应用场景
- 决策树分析:业务流程中的路径决策
- 游戏路径规划:RPG游戏中的任务路径
- 文件系统遍历:查找特定属性的文件路径
- 网络路由:寻找满足条件的网络路径
- 基因序列分析:DNA序列中的特定模式匹配
测试用例设计
基础测试
- 单节点树(正数、负数、零)
- 简单二叉树(有解、无解)
- 多路径树(多个有效路径)
边界测试
- 空树
- 目标和为0
- 所有节点值为负数
- 路径和溢出边界
性能测试
- 深度优先的极端情况(链式树)
- 广度优先的极端情况(满二叉树)
- 大规模随机树
实战技巧总结
- 路径管理:选择合适的路径维护策略(值传递vs引用传递)
- 回溯思想:理解回溯的本质是状态恢复
- 边界判断:准确识别叶子节点和空节点
- 结果收集:确保路径副本的正确性
- 性能权衡:在代码简洁性和性能之间找到平衡
- 测试覆盖:考虑各种边界情况和特殊输入
核心洞察
这道题的核心在于理解树的遍历和路径状态管理,通过DFS+回溯的经典模式,系统地探索所有可能路径,体现了递归分解和状态恢复的重要思想,是掌握树形结构算法的关键题目。
完整题解代码
package mainimport ("fmt""reflect""strings""time"
)// TreeNode 二叉树节点定义
type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode
}// ========== 方法1: DFS递归回溯(值传递) ==========
func pathSum1(root *TreeNode, targetSum int) [][]int {var result [][]intvar path []intdfs1(root, targetSum, path, &result)return result
}func dfs1(node *TreeNode, remain int, path []int, result *[][]int) {if node == nil {return}// 添加当前节点到路径path = append(path, node.Val)remain -= node.Val// 如果是叶节点且路径和等于目标和if node.Left == nil && node.Right == nil && remain == 0 {// 复制路径,避免引用问题pathCopy := make([]int, len(path))copy(pathCopy, path)*result = append(*result, pathCopy)return}// 递归遍历左右子树dfs1(node.Left, remain, path, result)dfs1(node.Right, remain, path, result)
}// ========== 方法2: DFS递归回溯(引用传递,手动回溯) ==========
func pathSum2(root *TreeNode, targetSum int) [][]int {var result [][]intvar path []intdfs2(root, targetSum, &path, &result)return result
}func dfs2(node *TreeNode, remain int, path *[]int, result *[][]int) {if node == nil {return}// 添加当前节点到路径*path = append(*path, node.Val)remain -= node.Val// 如果是叶节点且路径和等于目标和if node.Left == nil && node.Right == nil && remain == 0 {pathCopy := make([]int, len(*path))copy(pathCopy, *path)*result = append(*result, pathCopy)} else {// 继续搜索子树dfs2(node.Left, remain, path, result)dfs2(node.Right, remain, path, result)}// 回溯:移除当前节点*path = (*path)[:len(*path)-1]
}// ========== 方法3: BFS层序遍历 ==========
func pathSum3(root *TreeNode, targetSum int) [][]int {if root == nil {return [][]int{}}var result [][]inttype pathInfo struct {node *TreeNodepath []intsum int}queue := []pathInfo{{root, []int{root.Val}, root.Val}}for len(queue) > 0 {current := queue[0]queue = queue[1:]node := current.nodepath := current.pathsum := current.sum// 如果是叶节点if node.Left == nil && node.Right == nil {if sum == targetSum {result = append(result, path)}continue}// 处理左子树if node.Left != nil {leftPath := make([]int, len(path))copy(leftPath, path)leftPath = append(leftPath, node.Left.Val)queue = append(queue, pathInfo{node: node.Left,path: leftPath,sum: sum + node.Left.Val,})}// 处理右子树if node.Right != nil {rightPath := make([]int, len(path))copy(rightPath, path)rightPath = append(rightPath, node.Right.Val)queue = append(queue, pathInfo{node: node.Right,path: rightPath,sum: sum + node.Right.Val,})}}return result
}// ========== 方法4: DFS迭代栈实现 ==========
func pathSum4(root *TreeNode, targetSum int) [][]int {if root == nil {return [][]int{}}var result [][]inttype stackInfo struct {node *TreeNodepath []intsum int}stack := []stackInfo{{root, []int{root.Val}, root.Val}}for len(stack) > 0 {current := stack[len(stack)-1]stack = stack[:len(stack)-1]node := current.nodepath := current.pathsum := current.sum// 如果是叶节点if node.Left == nil && node.Right == nil {if sum == targetSum {result = append(result, path)}continue}// 右子树先入栈(后处理)if node.Right != nil {rightPath := make([]int, len(path))copy(rightPath, path)rightPath = append(rightPath, node.Right.Val)stack = append(stack, stackInfo{node: node.Right,path: rightPath,sum: sum + node.Right.Val,})}// 左子树后入栈(先处理)if node.Left != nil {leftPath := make([]int, len(path))copy(leftPath, path)leftPath = append(leftPath, node.Left.Val)stack = append(stack, stackInfo{node: node.Left,path: leftPath,sum: sum + node.Left.Val,})}}return result
}// ========== 方法5: 优化DFS(预分配优化) ==========
func pathSum5(root *TreeNode, targetSum int) [][]int {var result [][]intpath := make([]int, 0, 1000) // 预分配容量dfs5(root, targetSum, path, &result)return result
}func dfs5(node *TreeNode, remain int, path []int, result *[][]int) {if node == nil {return}// 添加当前节点path = append(path, node.Val)remain -= node.Val// 检查叶节点if node.Left == nil && node.Right == nil && remain == 0 {pathCopy := make([]int, len(path))copy(pathCopy, path)*result = append(*result, pathCopy)return}// 继续搜索dfs5(node.Left, remain, path, result)dfs5(node.Right, remain, path, result)
}// ========== 工具函数 ==========// 根据数组构建二叉树
func buildTree(values []interface{}) *TreeNode {if len(values) == 0 {return nil}root := &TreeNode{Val: values[0].(int)}queue := []*TreeNode{root}index := 1for len(queue) > 0 && index < len(values) {node := queue[0]queue = queue[1:]// 左子节点if index < len(values) && values[index] != nil {node.Left = &TreeNode{Val: values[index].(int)}queue = append(queue, node.Left)}index++// 右子节点if index < len(values) && values[index] != nil {node.Right = &TreeNode{Val: values[index].(int)}queue = append(queue, node.Right)}index++}return root
}// 层序打印树结构
func printTree(root *TreeNode) {if root == nil {fmt.Println("Empty tree")return}queue := []*TreeNode{root}level := 0for len(queue) > 0 {size := len(queue)fmt.Printf("Level %d: ", level)allNull := truefor i := 0; i < size; i++ {node := queue[0]queue = queue[1:]if node != nil {fmt.Printf("%d ", node.Val)queue = append(queue, node.Left)queue = append(queue, node.Right)allNull = false} else {fmt.Print("null ")queue = append(queue, nil, nil)}}fmt.Println()level++// 如果下一层全是null,停止打印if allNull {break}}
}// 打印路径列表
func printPaths(paths [][]int) {if len(paths) == 0 {fmt.Println("[]")return}fmt.Print("[")for i, path := range paths {if i > 0 {fmt.Print(",")}fmt.Print("[")for j, val := range path {if j > 0 {fmt.Print(",")}fmt.Print(val)}fmt.Print("]")}fmt.Println("]")
}// 比较两个路径列表是否相等
func equalPaths(paths1, paths2 [][]int) bool {if len(paths1) != len(paths2) {return false}// 转换为字符串集合进行比较(顺序无关)set1 := make(map[string]bool)set2 := make(map[string]bool)for _, path := range paths1 {set1[fmt.Sprintf("%v", path)] = true}for _, path := range paths2 {set2[fmt.Sprintf("%v", path)] = true}return reflect.DeepEqual(set1, set2)
}// ========== 测试和性能评估 ==========
func main() {// 测试用例testCases := []struct {name stringtreeVals []interface{}targetSum intexpected [][]int}{{name: "示例1: 复杂二叉树",treeVals: []interface{}{5, 4, 8, 11, nil, 13, 4, 7, 2, nil, nil, nil, nil, 5, 1},targetSum: 22,expected: [][]int{{5, 4, 11, 2}},},{name: "示例2: 无解情况",treeVals: []interface{}{1, 2, 3},targetSum: 5,expected: [][]int{},},{name: "示例3: 目标和为0",treeVals: []interface{}{1, 2},targetSum: 0,expected: [][]int{},},{name: "测试4: 单节点树",treeVals: []interface{}{5},targetSum: 5,expected: [][]int{{5}},},{name: "测试5: 单节点不匹配",treeVals: []interface{}{5},targetSum: 3,expected: [][]int{},},{name: "测试6: 负数节点",treeVals: []interface{}{1, -2, -3, 1, 3, -2, nil, -1},targetSum: 2,expected: [][]int{{1, -2, 3}},},{name: "测试7: 全负数",treeVals: []interface{}{-1, -2, -3},targetSum: -3,expected: [][]int{{-1, -2}},},{name: "测试8: 链式结构",treeVals: []interface{}{1, 2, nil, 3, nil, 4, nil},targetSum: 10,expected: [][]int{{1, 2, 3, 4}},},{name: "测试9: 多路径相同和",treeVals: []interface{}{10, 5, 15, 3, 7, nil, 18},targetSum: 18,expected: [][]int{{10, 5, 3}},},{name: "测试10: 空树",treeVals: []interface{}{},targetSum: 0,expected: [][]int{},},}// 算法方法methods := []struct {name stringfn func(*TreeNode, int) [][]int}{{"DFS递归回溯(值传递)", pathSum1},{"DFS递归回溯(引用传递)", pathSum2},{"BFS层序遍历", pathSum3},{"DFS迭代栈", pathSum4},{"优化DFS", pathSum5},}fmt.Println("=== LeetCode 113. 路径总和 II - 测试结果 ===")fmt.Println()// 运行测试for _, tc := range testCases {fmt.Printf("测试用例: %s\n", tc.name)fmt.Printf("目标和: %d\n", tc.targetSum)// 构建测试树root := buildTree(tc.treeVals)fmt.Println("二叉树结构:")printTree(root)allPassed := truevar results [][][]intvar times []time.Durationfor _, method := range methods {start := time.Now()result := method.fn(root, tc.targetSum)elapsed := time.Since(start)results = append(results, result)times = append(times, elapsed)status := "✅"if !equalPaths(result, tc.expected) {status = "❌"allPassed = false}fmt.Printf(" %s: %s (耗时: %v)\n", method.name, status, elapsed)fmt.Print(" 结果: ")printPaths(result)}fmt.Print("期望结果: ")printPaths(tc.expected)if allPassed {fmt.Println("✅ 所有方法均通过")} else {fmt.Println("❌ 存在失败的方法")}fmt.Println(strings.Repeat("-", 60))}// 性能对比测试fmt.Println("\n=== 性能对比测试 ===")performanceTest()// 算法特性总结fmt.Println("\n=== 算法特性总结 ===")fmt.Println("1. DFS递归回溯(值传递):")fmt.Println(" - 时间复杂度: O(N)")fmt.Println(" - 空间复杂度: O(H)")fmt.Println(" - 特点: 代码简洁,自动回溯")fmt.Println()fmt.Println("2. DFS递归回溯(引用传递):")fmt.Println(" - 时间复杂度: O(N)")fmt.Println(" - 空间复杂度: O(H)")fmt.Println(" - 特点: 空间效率高,需手动回溯")fmt.Println()fmt.Println("3. BFS层序遍历:")fmt.Println(" - 时间复杂度: O(N)")fmt.Println(" - 空间复杂度: O(W)")fmt.Println(" - 特点: 层次处理,适合宽树")fmt.Println()fmt.Println("4. DFS迭代栈:")fmt.Println(" - 时间复杂度: O(N)")fmt.Println(" - 空间复杂度: O(H)")fmt.Println(" - 特点: 显式栈,避免递归")fmt.Println()fmt.Println("5. 优化DFS:")fmt.Println(" - 时间复杂度: O(N)")fmt.Println(" - 空间复杂度: O(H)")fmt.Println(" - 特点: 预分配优化,减少内存分配")// 路径搜索演示fmt.Println("\n=== 路径搜索演示 ===")demoPathSearch()
}// 性能测试
func performanceTest() {sizes := []int{100, 500, 1000, 2000}methods := []struct {name stringfn func(*TreeNode, int) [][]int}{{"DFS值传递", pathSum1},{"DFS引用传递", pathSum2},{"BFS层序", pathSum3},{"DFS迭代", pathSum4},{"优化DFS", pathSum5},}for _, size := range sizes {fmt.Printf("性能测试 - 树大小: %d个节点\n", size)// 构建测试树root := buildBalancedTree(size)targetSum := size + 100 // 不太可能匹配的目标和for _, method := range methods {start := time.Now()result := method.fn(root, targetSum)elapsed := time.Since(start)fmt.Printf(" %s: 找到%d条路径, 耗时=%v\n",method.name, len(result), elapsed)}}
}// 构建平衡测试树
func buildBalancedTree(size int) *TreeNode {if size <= 0 {return nil}values := make([]interface{}, size)for i := 0; i < size; i++ {values[i] = i + 1}return buildTree(values)
}// 路径搜索演示
func demoPathSearch() {fmt.Println("构建示例树:")treeVals := []interface{}{5, 4, 8, 11, nil, 13, 4, 7, 2, nil, nil, nil, nil, 5, 1}root := buildTree(treeVals)printTree(root)fmt.Println("寻找路径和为 22 的所有路径:")result := pathSum1(root, 22)fmt.Printf("找到 %d 条有效路径:\n", len(result))for i, path := range result {sum := 0for _, val := range path {sum += val}fmt.Printf("路径 %d: %v (和=%d)\n", i+1, path, sum)}fmt.Println("路径搜索完成!")
}