【代码随想录训练营】【Day18】第六章|二叉树|513.找树左下角的值|112. 路径总和|113.路径总和ii|105.从前序与中序遍历序列构造二叉树|106.从中序与后序遍历序列构造二叉树

找树左下角的值

题目详细:LeetCode.513

由题可知:找出该二叉树的最底层、最左边节点的值

  • 最底层:节点所处的深度最深
  • 最左边:统一深度的节点,优先取最左边的节点

因此,我们只要找到满足这两个条件的节点就是树的左下角的节点,难点在于如何找?

  • 找最底层:定义一个变量,用于记录遍历到的节点的深度,以此来确定当前节点是不是最底层的节点
  • 找最左边:优先找最左边的节点,那么在访问子节点时,左节点要优先于右节点被访问,所以这道题可以使用多种不同的遍历方式,如前序、后序等等,只要保证左节点优先被访问就行。

这道题使用迭代的方式,比递归要清晰易懂些。

既然我们要找最底层、最左边的节点,那么层序遍历从上到下、从左到右的特点毫无疑问满足了我们所有的需求,只需要在遍历过程中,一直记录每一层最先访问的节点,那么当树遍历完时,返回记录的节点即是树左小角的值。

Java解法(迭代、层序遍历):

class Solution {
    public int findBottomLeftValue(TreeNode root) {
        return this.bfd(root);
    }

    public int bfd(TreeNode root){
        Queue<TreeNode> queue = new LinkedList<>();
        if(null != root) queue.offer(root);
        int leftNum = 0;
        while(!queue.isEmpty()){
            int n = queue.size();
            boolean f_switch = true;
            while(n-- > 0){
                TreeNode node = queue.poll();
                // 每一层从左到右访问,所以每一层最先访问的节点就是最左边的节点
                if(f_switch){
                    leftNum = node.val;
                    f_switch = false;
                }
                if(null != node.left) queue.offer(node.left);
                if(null != node.right) queue.offer(node.right);
            }
        }
        return leftNum;
    }
}

也可以使用递归,不过我们需要改动一下代码:

  • 在递归函数中增加一个深度参数,记录当前节点的深度,在遍历过程中比较节点的深度,保证节点位于最底层的特点
  • 采用前序遍历(或后序遍历),使每一次递归都优先访问左节点,保证节点位于最左边的特点
  • 优化:因为最底层的节点一定是叶子节点,所以我们可以在遍历到叶子节点时,再比较已遍历的最大深度和当前节点的深度,以此来判断该叶子节点是否为最底层的节点。

Java解法(递归,前序遍历):

class Solution {
    public int maxDepth = 0;
    public int ans = 0;

    public int findBottomLeftValue(TreeNode root) {
        this.dfs(root, 1);
        return this.ans;
    }

    public void dfs(TreeNode root, int depth){
        if(null == root) return;
        if(null == root.left && null == root.right){
            if(depth > this.maxDepth){
                this.ans = root.val;
                this.maxDepth = depth;
            }
        }
        if(null != root.left) this.dfs(root.left, depth + 1);
        if(null != root.right) this.dfs(root.right, depth + 1);
    }
}

路径总和

题目详细:LeetCode.112

由题可知:只要求判断树中是否能够找到一条路径总和满足目标值的路径

那么这道题的思路就比较简单了,就是对树进行深度优先遍历:

  • 一边遍历一边累计路径上的节点值,到达叶子节点时,将路径总和和目标值做比较
  • 当路径总和和目标值不想等时,说明当前路径是错误的
  • 这时就需要回溯到上一个节点,进入其他路径,计算其他的路径总和
  • 当存在一条路径总和与目标值相等时,返回true,否则返回false

由思路可知本题涉及到深度优先遍历和回溯法,那么我们可以利用递归法来解题:

Java解法(递归,深度优先遍历,前序遍历):

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        return dfs(root, targetSum, 0);
    }

    public boolean dfs(TreeNode root, int targetSum, int curSum){
        if(null == root) return false;
        curSum += root.val;
        if(null == root.left && null == root.right){
            if(curSum == targetSum) return true;
            return false;
        }
        return dfs(root.left, targetSum, curSum) || dfs(root.right, targetSum, curSum);
    }
}

Java解法(递归,回溯明显化):

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        return dfs(root, targetSum, 0);
    }

    public boolean dfs(TreeNode root, int targetSum, int curSum){
        if(null == root) return false;
        curSum += root.val;
        if(null == root.left && null == root.right){
            if(curSum == targetSum) return true;
            return false;
        }
        boolean leftFlag = dfs(root.left, targetSum, curSum);
        curSum -= root.val; // 回溯
        curSum += root.val;
        boolean rightFlag = dfs(root.right, targetSum, curSum);
        curSum -= root.val; // 回溯
        return leftFlag || rightFlag;
    }
}

路径总和ii

题目详细:LeetCode.113

这道题与上一道题的不同点就在于:要求记录并输出所有满足目标总和的路径

仅此而已,那么把上一道题稍微一变:

  • 在递归函数中增加两个参数:
    • 参数一:路径列表,用来存放路径上的节点,表示一条路径
    • 参数二:结果集,用来存放满足条件的所有路径
  • 当到达叶子节点时
    • 比较路径总和是否与目标总和相等,相等则添加到结果集
    • 否则回溯到上一个节点,进入其他路径,计算其他的路径总和
  • 直到所有节点都经历了访问和回溯,说明树遍历完毕,输出结果集即可

Java解法(递归,深度优先遍历):

class Solution {
    public int target;

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<Integer> path = new ArrayList<>();
        List<List<Integer>> ans = new ArrayList<>();
        this.target = targetSum;
        this.dfs(root, path, 0, ans);
        return ans;
    }

    public void dfs(TreeNode root, List<Integer> path, int sum, List<List<Integer>> ans){
        if(null == root) return;
        path.add(root.val);
        sum += root.val;
        if(null == root.left && null == root.right){
            if(this.target == sum) ans.add(path);
        }
        // 对path进行拷贝后再传递,避免出现其他路径的节点(隐藏的回溯)
        // 同时也使得path不会指向同一个引用,避免后续操作对path对象的影响
        this.dfs(root.left, new ArrayList<Integer>(path), sum, ans);
        this.dfs(root.right, new ArrayList<Integer>(path), sum, ans);
    }
}

Java解法(递归,深度优先遍历,回溯明显化):

class Solution {
    public int target;

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<Integer> path = new ArrayList<>();
        List<List<Integer>> ans = new ArrayList<>();
        this.target = targetSum;
        this.dfs(root, path, ans);
        return ans;
    }

    public void dfs(TreeNode root, List<Integer> path, List<List<Integer>> ans){
        if(null == root) return;
        path.add(root.val);
        if(null == root.left && null == root.right){
            int pathSum = path.stream().reduce(0, (a,b) -> a + b);
            List<Integer> clonedPath = path.stream()
                                      .map(e -> Integer.valueOf(e))
                                      .collect(Collectors.toList());
            if(this.target == pathSum) ans.add(clonedPath);
        }
        this.dfs(root.left, path, ans);
        path.remove(path.size() - 1); // 回溯
        path.add(root.val);
        this.dfs(root.right, path, ans);
        path.remove(path.size() - 1); // 回溯
    }
}

这道题还有一个更加需要注意的点,要注意递归参数的拷贝形式:

  • 在Java中,拷贝的形式有两种,浅拷贝和深拷贝:
    • 浅拷贝:又称引用传递,将对象的内存地址作为参数传递,后序对该参数进行的操作都会影响到原对象;
    • 再举个浅拷贝的例子,也很重要,如果对列表进行浅拷贝,那么对列表中的元素也是浅拷贝操作,拷贝后的列表中的元素,与原列表中的元素都是来自同一个引用。
    • 深拷贝:又称值传递,将对象作为参数传递,即整个对象包括对象的属性都进行一次拷贝,与原对象包括内部的属性都是不同的引用,对该参数包括其属性进行的操作都不会影响到原对象一丝一毫。
    • Java默认的参数传递都是浅拷贝,需要我们自己去实现clone方法来进行深拷贝
    • 例如,举个深拷贝的例子:
      • 如果想对列表进行深拷贝,那么对列表中的元素也需要进行深拷贝操作,才是对列表完整的深拷贝

诸如 ArrayList 的 clone() 方法并不会进行深拷贝,而只会创建一个新的 ArrayList 对象,其中包含与原始 ArrayList 对象中的相同元素引用。
这意味着,如果原始 ArrayList 中的元素发生更改,克隆对象中对应的元素也会发生更改。

  • 如果需要实现深拷贝,可以使用以下方法:

在这里插入图片描述


从前序与中序遍历序列构造二叉树

题目详细:LeetCode.105

学习二叉树理论的时候,也了解过构建二叉树的方法:

  • 一棵唯一的二叉树可以通过中序遍历序列 + 前序遍历序列(或后序遍历序列)来构建
  • 前序遍历:顺序是根左右,所以其序列结果的节点分布状态,一定是根节点在前,子节点在后
  • 中序遍历:顺序是左根右,所以当我们得到根节点时,只要在中序序列中找到根节点的位置,那么在序列中,其根节点位置之前的节点都是该节点的左子树上的节点,其根节点位置之后的节点都是该节点的右子树上的节点

由此可得:

  • 构建一颗唯一的二叉树不仅需要前序序列来确定根节点,还需要中序序列来确定节点的左右分布
  • 前序序列和后序序列无法确定一颗唯一的二叉树,因为除了中序遍历序列以外,其他遍历序列都是无法确定树的子节点左右分布的,所以注意一定需要有中序遍历序列。

Java解法(递归):

class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, inorder);
    }

    // 查找数组中目标值的索引
    public static int findIndex(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) {
                return i;
            }
        }
        return -1;
    }

    public TreeNode build(int[] preorder, int[] inorder){
        // 特判,如果前序序列为空则说明是一棵空树
        if(preorder.length == 0) return null;
        int cur_val = preorder[0];
        TreeNode root = new TreeNode(cur_val);
        // 直到前序序列无法再分,说明到达叶子节点,开始回溯构建二叉树
        if(preorder.length == 1) return root;
        // 在中序遍历中确定当前节点的索引位置,以划分左子树和右子树的节点序列
        int in_index = findIndex(inorder, cur_val);
        preorder = Arrays.copyOfRange(preorder, 1, preorder.length);
        // 分割左右节点序列,先确定中序遍历序列中左右节点序列的长度后,才能确定前序遍历序列中子节点的长度
        int[] leftInorder = Arrays.copyOfRange(inorder, 0, in_index);
        int[] rightInorder = Arrays.copyOfRange(inorder, in_index + 1, inorder.length);
        int[] leftPreorder = Arrays.copyOfRange(preorder, 0, leftInorder.length);
        int[] rightPreorder = Arrays.copyOfRange(preorder, 0 + leftInorder.length, preorder.length);
        // 递归继续分割
        root.left = this.build(leftPreorder, leftInorder);
        root.right = this.build(rightPreorder, rightInorder);
        return root;
    }
}

从中序与后序遍历序列构造二叉树

题目详细:LeetCode.106

这一题与上一题有着异曲同工之妙,上一题是从中序与前序遍历序列构造二叉树,而这一题是从中序与后序遍历序列构造二叉树

不过两道题的主要问题都是一样的,区别就在于如何通过后序遍历序列来根节点?

由后序遍历的特点可知:

  • 后序遍历:顺序是左右根,所以其序列结果的节点分布状态,一定是子节点在前,根节点在后

那么我们只需要修改两行代码,让它从后序遍历序列的尾部开始确定根节点,其他的代码都不用修改了:

Java解法(递归):

class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        return build(postorder, inorder);
    }

    // 查找数组中目标值的索引
    public static int findIndex(int[] arr, int target) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == target) {
                return i;
            }
        }
        return -1;
    }

    public TreeNode build(int[] postorder, int[] inorder){
        // 特判,如果后序序列为空则说明是一棵空树
        if(postorder.length == 0) return null;
        int cur_val = postorder[postorder.length - 1];
        TreeNode root = new TreeNode(cur_val);
        // 直到后序序列无法再分,说明到达叶子节点,开始回溯构建二叉树
        if(postorder.length == 1) return root;
        // 在中序遍历中确定当前节点的索引位置,以划分左子树和右子树的节点序列
        int in_index = findIndex(inorder, cur_val);
        postorder = Arrays.copyOfRange(postorder, 0, postorder.length - 1);
        // 分割左右节点序列,先确定中序遍历序列中左右节点序列的长度后,才能确定后序遍历序列中子节点的长度
        int[] leftInorder = Arrays.copyOfRange(inorder, 0, in_index);
        int[] rightInorder = Arrays.copyOfRange(inorder, in_index + 1, inorder.length);
        int[] leftPostorder = Arrays.copyOfRange(postorder, 0, leftInorder.length);
        int[] rightPostorder = Arrays.copyOfRange(postorder, 0 + leftInorder.length, postorder.length);
        // 递归继续分割
        root.left = this.build(leftPostorder, leftInorder);
        root.right = this.build(rightPostorder, rightInorder);
        return root;
    }
}

时间过得真快啊,一天总感觉做了很多,但是却啥都没做,甚至想开始做时,却发现时间已经很晚了,什么时候才能让生活得更有效率呢,真是一天24小时都不够用啊:

子在川上曰:逝者如斯夫,不舍昼夜。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值