代码随想录算法训练营day17||二叉树part04、110.平衡二叉树 、257. 二叉树的所有路径 、404.左叶子之和

注意:迭代法,可以先过,二刷有精力的时候 再去掌握迭代法。

110.平衡二叉树 (优先掌握递归)

再一次涉及到,什么是高度,什么是深度,可以巩固一下。

题目:给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

示例 1:给定二叉树 [3,9,20,null,null,15,7]    返回 true 。  示例2:返回false

题外话

咋眼一看这道题目和104.二叉树的最大深度 (opens new window)很像,其实有很大区别。

这里强调一波概念:

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。

但leetcode中强调的深度和高度很明显是按照节点来计算的,如图:

注意:关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。

因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)

有的同学一定疑惑,为什么104.二叉树的最大深度 (opens new window)中求的是二叉树的最大深度,也用的是后序遍历。

那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这棵树的最大深度,所以才可以使用后序遍历。

本题思路:(此时大家应该明白了既然要求比较高度,必然是要后序遍历。)采用递归法

递归三步曲分析:

1.明确递归函数的参数和返回值

参数:当前传入节点。 返回值:以当前传入节点为根节点的树的高度。

那么如何标记左右子树是否差值大于1呢?

如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。

所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。

2.明确终止条件

递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0

3.明确单层递归的逻辑

如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。

分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。

class Solution {
    public boolean isBalanced(TreeNode root) {
        //递归法
        return getHeight(root) != -1;
    }

    private int getHeight(TreeNode root){
        if(root == null ) return 0;

        int leftHeight = getHeight(root.left);
        if(leftHeight == -1) return -1;

        int rightHeight = getHeight(root.right);
        if(rightHeight == -1) return -1;

        //左右子树高度差大于1,return -1表示已经不是平衡二叉树了
        if(Math.abs(leftHeight - rightHeight) > 1)  {
            return -1;
        }
        return Math.max(leftHeight,rightHeight) + 1;
    }
}

257. 二叉树的所有路径 (优先掌握递归)  

这是大家第一次接触到回溯的过程, 我在视频里重点讲解了 本题为什么要有回溯,已经回溯的过程。 如果对回溯 似懂非懂,没关系, 可以先有个印象。 

题目:给定一个二叉树,返回所有从根节点到叶子节点的路径。

说明: 叶子节点是指没有子节点的节点。

思路

这道题目要求从根节点到叶子的路径,所以需要前序遍历这样才方便让父节点指向孩子节点,找到对应的路径。

在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。

前序遍历以及回溯的过程如图:

我们先使用递归的方式,来做前序遍历。 要知道递归和回溯就是一家的,本题也需要回溯。
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        //递归法
        List<String> res = new ArrayList<>(); //用于存储最终结果集的List,其中每个元素都是一个表示路径的字符串。
        if(root == null)  return res;
        List<Integer> paths = new ArrayList<>();//用于在递归过程中构建路径的List,其中每个元素都是二叉树节点的值。
        traversal(root,paths,res);
        return res;
    }
    //traversal 方法是递归的核心部分,它接受当前节点、路径列表和结果列表作为参数。
    private void traversal(TreeNode root,List<Integer> paths,List<String> res){
        paths.add(root.val); //前序遍历,中!
        //遇到叶子节点时(左右子树都为空的节点)
        if(root.left == null && root.right == null){
            //输出
            StringBuilder sb = new StringBuilder();//创建一个StringBuilder对象sb用来拼接字符串,速度更快
            //遍历路径列表,将paths中的元素按顺序连接成一个路径,并将该路径添加到res集合中。
            for(int i = 0;i < paths.size()-1; i++){ 
                sb.append(paths.get(i)).append("->");
            }
            sb.append(paths.get(paths.size() - 1));//记录最后一个节点
            res.add(sb.toString());//收集一个路径
            return;
        }
        //递归和回溯是同时进行的,所以要放在同一个花括号里
        if(root.left != null){ 
            traversal(root.left,paths,res); //左
            paths.remove(paths.size() - 1); //回溯
        }
        if(root.right != null){
            traversal(root.right,paths,res); //右
            paths.remove(paths.size() - 1); //回溯
        }
    }
}

注意:

  • traversal 方法:

    • traversal 方法是递归的核心部分。它接受当前节点、路径列表和结果列表作为参数。
    • 将当前节点的值添加到路径列表中。
    • 如果当前节点是叶子节点(左右子节点都为空),则执行以下操作:
      • 创建一个 StringBuilder 对象 sb
      • 通过遍历路径列表,将节点值连接成路径字符串,并使用 -> 分隔。
      • 将路径字符串添加到结果列表中。
    • 无论当前节点是否为叶子节点,都会递归地对左子节点和右子节点执行相同的操作(如果存在的话)。在递归之后,会通过 paths.remove(paths.size() - 1); 进行回溯,移除刚刚添加的节点值,以恢复到上一层的状态。

这行代码 paths.remove(paths.size() - 1);

是在回溯过程中使用的。在递归遍历左子树或右子树之后,程序需要回到当前节点的父节点,继续遍历其他子节点或者回溯到更上层的节点。

因为 paths 列表记录了从根节点到当前节点的路径,当递归返回到父节点时,需要将当前节点从路径中移除,以便继续探索其他分支或者回溯到更上层的节点,保证 paths 列表中记录的是当前路径上的节点序列。这就是为什么在回溯时,需要移除 paths 列表中的最后一个元素的原因。

class Solution {
    //方式二:递归法,精简版,并隐藏了回溯过程
    List<String> result = new ArrayList<>();//用于存储最终结果集的List,其中每个元素都是一个表示路径的字符串。

    public List<String> binaryTreePaths(TreeNode root) {
        traversal(root, "");
        return result;
    }
    public void traversal(TreeNode node, String sb) {
        //当前节点为空时
        if (node == null)
            return;
        //当前节点为 叶子节点时,将当前节点的值添加到字符串s后面,并将整个字符串添加到结果列表中。
        if (node.left == null && node.right == null) {
            result.add(new StringBuilder(sb).append(node.val).toString()); //中
            return;
        }
        //创建一个临时字符串 tmp,它是在字符串s后面添加了当前节点的值和箭头 ->。
        String tmp = new StringBuilder(sb).append(node.val).append("->").toString();  
        traversal(node.left, tmp);  //左
        traversal(node.right, tmp); //右
    }
}
  1. traversal 方法:

    • 如果当前节点为空,直接返回。
    • 如果当前节点是叶子节点(左右子节点都为空),则将当前节点的值添加到字符串 s 后面,并将整个字符串添加到结果列表中。
    • 创建一个临时字符串 tmp,它是在字符串 s 后面添加了当前节点的值和 ->
    • 然后分别对左子节点和右子节点递归调用 traversal 方法,并将临时字符串 tmp 作为参数传递下去。
  2. 总体逻辑:

    • 通过递归调用 traversal 方法,在每个叶子节点处将路径字符串添加到结果列表中。
    • 递归过程中使用临时字符串来构建路径,简化了代码的实现。

这种方法的核心思想与之前的代码类似,都是通过递归遍历二叉树,并在叶子节点处构建路径字符串。但是,这个精简版的代码隐藏了回溯过程,通过临时字符串tmp和直接在叶子节点处添加路径字符串来简化了代码的结构。

404.左叶子之和 (优先掌握递归)

其实本题有点文字游戏,搞清楚什么是左叶子,剩下的就是二叉树的基本操作。 

计算给定二叉树的所有左叶子之和。

思路

首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。

因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点

大家思考一下如下图中二叉树,左叶子之和究竟是多少? 没有左叶子!

 再看这个图的左叶子之和是多少?

相信通过这两个图,大家对最左叶子的定义有明确理解了。

那么判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。

如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子,判断代码如下:

if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) {
    左叶子节点处理逻辑
}

递归法

递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。

递归三部曲:

1.确定递归函数的参数和返回值

判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int

使用题目中给出的函数就可以了。

2.确定终止条件

如果遍历到空节点,那么左叶子值一定是0

注意,只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子。 所以如果当前遍历的节点是叶子节点,那其左叶子也必定是0,那么终止条件为:

if (root == NULL) return 0;
if (root->left == NULL && root->right== NULL) return 0; //其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。

3.确定单层递归的逻辑

当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        //递归法,后序遍历
        if(root == null) return 0;
        int leftValue = sumOfLeftLeaves(root.left); //左
        int rightValue = sumOfLeftLeaves(root.right); //右

        int midValue = 0;
        //判断左叶子的关键语句,该节点的左节点不为空,但左节点的左节点和左节点的右节点为空! 
        //则把该节点的左节点赋值给midValue
        if(root.left != null && root.left.left == null && root.left.right == null){
            midValue = root.left.val;
        }
        int sum = midValue + leftValue + rightValue; //中
        return sum;
    }
}
  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值