112. 路径总和

112. 路径总和

112. 路径总和

给你二叉树的根节点root和一个表示目标和的整数targetSum。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和targetSum。如果存在,返回 true ;否则,返回 false

叶子节点 是指没有子节点的节点。

示例 1:
在这里插入图片描述

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

示例 2:
在这里插入图片描述

输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。

示例 3:

输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。

提示:

  • 树中节点的数目在范围 [0, 5000] 内
  • -1000 <= Node.val <= 1000
  • -1000 <= targetSum <= 1000

思路

相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。

那么接下来我通过详细讲解如下两道题,来回答这个问题:

  • 112.路径总和
  • 113.路径总和ii

这道题我们要遍历从根节点到叶子节点的路径看看总和是不是目标和。

递归

可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树

  1. 确定递归函数的参数和返回类型
  • 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

  • 再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

    • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
    • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在104.二叉树的最大深度(包括N叉树的最大深度)中遇到过),且当递归函数的参数和返回值与题目给的函数定义一致是,都不需要自己再定一个dfs函数了,直接用题目给的即可。只有递归函数参数或者返回值与题目所给不同时,我们就必须定义一个dfs函数。
    • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?

如图所示:

在这里插入图片描述

图中可以看出,遍历的路线,并不要遍历整棵树,且需要上层节点能感知到经过自身节点的路径到达叶子节点后是否存在符合条件的路径,所以递归函数需要返回值,可以用bool类型表示,在回归的过程中,上层节点就可以继续往上回归告知再上一层节点,从我这里过的话存在满足条件的路径,知道一直回归告知给根节点,便可以知道整棵树存在满足条件的路径了。

所以代码如下(和题目给的参数与返回值一致,不需要额外定义函数了):

func hasPathSum(root *TreeNode, targetSum int) bool {}
  1. 确定终止条件

首先计数器如何统计这一条路径的和呢?

不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器targetSum初始为目标和,然后每次减去遍历路径节点上的数值。

如果遍历到了空节点之前都没有return,就是没找到。

如果最后到达叶子节点时,targetSum - root.Val == 0,说明找到了目标和。

递归终止条件代码如下:

if root == nil {
        return false
    }

// 遇到叶子节点,并且减去当前叶子节点的值后计数刚好为0
if root.Left == nil && root.Right == nil && targetSum - root.Val == 0 {
    return true
}
  1. 确定单层递归的逻辑

递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,只要左右子树任意一棵子树返回true,就可以认为就可以一路回归回去告知根节点了。

代码如下:

left := hasPathSum(root.Left,targetSum - root.Val)
right := hasPathSum(root.Right,targetSum - root.Val)
return left || right

以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。

回溯隐藏在hasPathSum(root.Left,targetSum - root.Val)这里, 因为把targetSum - root.Val直接作为参数传进去,函数结束,targetSum的数值没有改变。

为了把回溯的过程体现出来,可以改为如下代码:

整体Go代码如下:

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil {
        return false
    }

    // 遇到叶子节点,并且减去当前叶子节点的值后计数刚好为0
    if root.Left == nil && root.Right == nil && targetSum - root.Val == 0 {
        return true
    }
    
    targetSum = targetSum - root.Val // 递归,处理节点
    left := hasPathSum(root.Left,targetSum - root.Val) // 左
    targetSum = targetSum + root.Val // 回溯,撤销处理结果

    targetSum = targetSum - root.Val // 递归,处理节点
    right := hasPathSum(root.Right,targetSum - root.Val) // 右
    targetSum = targetSum + root.Val // 回溯,撤销处理结果
    return left || right
}

以上代码精简之后如下:

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil {
        return false
    }

    // 遇到叶子节点,并且减去当前叶子节点的值后计数刚好为0
    if root.Left == nil && root.Right == nil && targetSum - root.Val == 0 {
        return true
    }
    
   return hasPathSum(root.Left,targetSum - root.Val) || hasPathSum(root.Right,targetSum - root.Val)
}

这里最后一行精简后还有一个好处,那就是“||”前面如果是true了,右子树都不用去递归了,但是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,再追求代码精简。 这一点我已经强调很多次了!

在这里插入图片描述

113. 路径总和 II

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

思路

题中所指的路径是从根节点出发到叶节点,也就是说路径总是以根节点为起始点,以叶子节点为终点,因此我们首先要遍历根节点。纵观树的前、中、后序三种遍历方式中,只有前序遍历是首先访问根节点的,所以我们应选择前序遍历

当用前序遍历的方式访问到某一节点时,我们把该节点添加到路径list中,并更新sum的值,如果该节点是叶子节点,且sum此时为0了,说明当前的路径符合要求,我们当前路径加入到res中。如果当前节点不是叶节点,则继续访问他的子节点。当前节点访问结束后,递归函数会自动回到他的父节点(而不要使用return返回,代码中有注释详解说明原因)。我们需要进行现场恢复

整个遍历执行图如下:实线是,虚线是,大家可以跟着看一遍,对二叉树遍历会更有体感。
在这里插入图片描述

递归

Go代码

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func pathSum(root *TreeNode, targetSum int) [][]int {
    if root == nil {
        return [][]int{}
    }

    //深度优先遍历
    res := make([][]int,0)
    curList := make([]int,0)
    dfs(root,&res,&curList,targetSum)
    
    return res
}

func dfs(root *TreeNode,res *[][]int,curList *[]int,targetSum int) {
    *curList = append(*curList,root.Val)
    targetSum -= root.Val
    if root.Left == nil && root.Right == nil && targetSum == 0 {
        // 一定要注意这里加的是副本 以及 学习这里添加列表副本的技巧
        *res = append(*res,append([]int(nil),*curList...))
        //return //此处不能写return 因为还需要执行后面的两个dfs和还原语句,
        //这样才能执行到*curList = (*curList)[0:len(*curList) - 1],把叶子节点删掉
        // 从而还原回上一级栈的curList
    }

    if root.Left != nil {
        dfs(root.Left,res,curList,targetSum)
    }
    if root.Right != nil {
        dfs(root.Right,res,curList,targetSum)
    }

    //回溯时还原为上级运行栈的curList
    //targetSum无需还原,因为targetSum是值传递,实参本身就没变,变的是形参
    *curList = (*curList)[0:len(*curList) - 1]
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值