代码随想录day18
[坚持比较重要]
目录
513.找树左下角的值
给定一个二叉树,在树的最后一行找到最左边的值。
示例 1:
示例2:
思路:
层序遍历的话就是把取最后一层的第一个值就行,递归法暂略。
func findBottomLeftValue(root *TreeNode) int {
queue := list.New()
res := 0
queue.PushBack(root)
for queue.Len() > 0 {
length := queue.Len()
for i:=0;i<length;i++ {
node := queue.Remove(queue.Front()).(*TreeNode)
if i == 0 {
res = node.Val // 每层的第一个值更新给res。
}
if node.Left != nil {
queue.PushBack(node.Left)
}
if node.Right != nil {
queue.PushBack(node.Right)
}
}
}
return res
}
112、路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例: 给定如下二叉树,以及目标和 sum = 22,返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2
思路:
相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。
那么接下来我通过详细讲解如下两道题,来回答这个问题:
- 112.路径总和
- 113.路径总和ii
这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。
这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。
可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树。
1、确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
- 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
- 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先中介绍)
- 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?
如图所示:
图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。
所以代码如下:
func digui(root *TreeNode, count int) bool {
}
2、确定终止条件
首先计数器如何统计这一条路径的和呢?
不要去累加然后判断是否等于目标和,那样代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。
如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。
如果遍历到了叶子节点,count不为0,就是没找到。
递归终止条件代码如下:
if root.Left == nil && root.Right == nil && count == 0 { //遇到叶子节点并且计数为0
return true
}
if root.Left == nil && root.Right == nil && count != 0 { //遇到叶子节点并且计数不为0
return false
}
3、确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
代码如下:
if root.Left != nil {
if digui(root.Left, count - root.Left.Val) {
return true
}
}
if root.Right != nil {
if digui(root.Rigjt, count - root.Right.Val) {
return true
}
}
return false
以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。
回溯隐藏在traversal(cur->left, count - cur->left->val)这里, 因为把count - cur->left->val 直接作为参数传进去,函数结束,count的数值没有改变。
为了把回溯的过程体现出来,可以改为如下代码:
if root.Left != nil {
count -= root.Left.Val //递归,处理节点
if digui(root.Left, count){
return true
}
count += root.Left.val //回溯,撤销处理结果
}
if root.Right != nil {
count -= root.Right.Val
if digui(root.Right, count) {
return true
}
count += root.Right.Val
}
整体代码如下:这道题没有中的处理逻辑,所以前后中序都是一样代码啊。
// 深度优先遍历二叉树,每深入一次,sum-根节点的值,当到达叶子节点的时候,判断count是否为0(count是记录targetSum - path.Val),如果为0说明找到了,否则尝试另外一条路径
func hasPathSum(root *TreeNode, targetSum int) bool {
if root == nil {
return false
}
return digui(root, targetSum - root.Val)
}
func digui(root *TreeNode, count int) bool {
if root.Left == nil && root.Right == nil && count == 0 {
return true // 找到符合条件的路径
}
if root.Left == nil && root.Right == nil && count != 0 {
return false
}
if root.Left != nil {
count -= root.Left.Val
if digui(root.Left, count) {
return true
}
count += root.Left.Val
}
if root.Right != nil {
count -= root.Right.Val
if digui(root.Right, count) {
return true
}
count += root.Right.Val
}
return false
}
113、路径总和2
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
示例:给定如下二叉树,以及目标和 sum = 22,
思路:
这道题和前面112那道的区别是,上一道只要找到符合条件的路径就return true。这道是要求找到所有符合条件的path,然后return 包含所有path的集合。所以要遍历整个树的,要遍历整个树的话递归函数就不要有返回值。
var result [][]int
var path []int
func pathSum(root *TreeNode, targetSum int) [][]int {
result = [][]int{}
path = []int{}
if root == nil {
return result
}
path = append(path, root.Val)
digui(root, targetSum - root.Val)
return result
}
func digui(root *TreeNode, count int) {
if root.Left == nil && root.Right == nil && count == 0 {
temp := make([]int,len(path))
copy(temp, path)
result = append(result, temp)
return
}
if root.Left == nil && root.Right == nil && count != 0 {
return
}
if root.Left != nil {
path = append(path, root.Left.Val)
count -= root.Left.Val
digui(root.Left,count)
count += root.Left.Val
path = path[:len(path)-1]
}
if root.Right != nil {
path = append(path, root.Right.Val)
count -= root.Right.Val
digui(root.Right,count)
count += root.Right.Val
path = path[:len(path)-1]
}
return
}
小结:
本篇通过leetcode上112. 路径总和 和 113. 路径总和ii 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。
这两道题目是掌握这一知识点非常好的题目,大家看完本篇文章再去做题,就会感受到搜索整棵树和搜索某一路径的差别。
对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了!
106、从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树。注意:你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
思路:
解决此问题的关键在于要很熟悉树的各种遍历次序代表的什么,最好能够将图画出来。本题解带你先进行中序遍历和后续遍历二叉树,然后再根据遍历结果将二叉树进行还原。
首先,来一棵树
由上面可知,后序遍历的最后一个节点是中序遍历的根结点。这一点很重要哦。
我们要根据中序和后序遍历结果还原二叉树,那么我们就要利用中序遍历和后续遍历的特性
树的还原过程描述
根据中序遍历和后续遍历的特性我们进行树的还原过程分析
- 首先在后序遍历序列中找到根节点(最后一个元素)
- 根据根节点在中序遍历序列中找到根节点的位置
- 根据根节点的位置将中序遍历序列分为左子树和右子树
- 根据根节点的位置确定左子树和右子树在中序数组和后续数组中的左右边界位置
- 递归构造左子树和右子树
- 返回根节点结束
树的还原过程
// 1.后序遍历的最后一个值为树根。
// 2.从中序中找到树根所在的下标,作为切割点。、
// 3.自行确定inorder和postorder的左右子树的边界
// 4.递归构建左右子树
// 5.返回树
func buildTree(inorder []int, postorder []int) *TreeNode {
if len(inorder) < 1 || len(postorder) < 1 {
return nil
}
node := postorder[len(postorder)-1] //第1步
cutpoint := findCutPoint(inorder, node) //第2步
root := &TreeNode{ //第3、第4步
Val: node,
Left:buildTree(inorder[:cutpoint], postorder[:cutpoint]),
Right:buildTree(inorder[cutpoint+1:], postorder[cutpoint:len(postorder)-1]),
}
return root //第5步
}
func findCutPoint(inorder []int, node int) int {
for i, v := range inorder {
if v == node {
return i
}
}
return -1
}
105、从前序遍历和中序遍历中构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
思路:
这道题和前面的后序遍历+中序遍历构造二叉树是一样的。就是找root的第一个值变成前序的第一个值以及cutpoint下标有点区别罢了。
func buildTree(preorder []int, inorder []int) *TreeNode {
if len(inorder) < 1 || len(preorder) < 1 {
return nil
}
node := preorder[0]
cutpoint := findCutPoint(inorder, node)
root := &TreeNode{Val:node,
Left:buildTree(preorder[1:cutpoint+1], inorder[:cutpoint]),
Right:buildTree(preorder[cutpoint+1:], inorder[cutpoint+1:]),
}
return root
}
func findCutPoint(inorder []int, node int ) int {
for i, v := range inorder{
if v == node {
return i
}
}
return -1
}