02_二叉树的性质

本文详细探讨了二叉树的各种操作,包括对称性检查、结构比较、平衡性和完全性验证,以及计算节点数量、路径和左叶子节点之和。通过迭代和递归两种方法,展示了如何高效地解决这些问题,涉及前序、中序和后序遍历,以及层序遍历的应用。此外,还介绍了寻找二叉树中目标路径总和的方法以及最近公共祖先的求解策略。
摘要由CSDN通过智能技术生成

01.二叉树是否对称、相同

对称和相同的处理框架相同,迭代法使用辅助队列,递归法,对应递归的孩子节点;

练习题:

101. 对称二叉树

对称是关于根节点的轴对称,辅助队列,成对入队进行比较,辅助队列可以存取空节点;

迭代法,辅助队列,成对入队,成对比较,可以存入空值,增加对应的处理即可

    // 辅助队列,成对入队,成对比较,可以存入空值,增加对应的处理即可
    public boolean isSymmetric(TreeNode root) {
        // 如果根节点为空,默认对称
        if (root == null) {
            return true;
        } // 否则树非空,左右子树成对处理
        Queue<TreeNode> queue = new LinkedList<>();
        // 初始化辅助队列,此时可能存入空节点,所以迭代时,要增加判断,再取值
        queue.offer(root.left);
        queue.offer(root.right);
        // 循环条件是队列非空
        while (!queue.isEmpty()) {
            // 获取当前节点
            TreeNode currLeft = queue.poll();
            TreeNode currRight = queue.poll();
            // 罗列当前节点的所有情况:
            // 01.两个节点都为空,无需处理
            // 02.一个节点为空,一个节点非空,返回 false
            // 03.两个节点都非空,且节点值不同,返回 false
            // 04.两个节点都非空,且节点值相同,继续遍历其孩子节点,成对入队
            if ((currLeft == null) && (currRight == null)) {
                // 01.两个节点都为空,无需处理
                continue; // 无孩子节点存入,继续遍历其他节点
            } else if ((currLeft != null) && (currRight == null)) {
                // 02.一个节点为空,一个节点非空,返回 false
                return false;
            } else if ((currLeft == null) && (currRight != null)) {
                // 02.一个节点为空,一个节点非空,返回 false
                return false;
            } else if (currLeft.val != currRight.val) { // 此处节点都存在,可以取值
                // 03.两个节点都非空,且节点值不同,返回 false
                return false;
            } else {
                // 04.两个节点都非空,且节点值相同,继续遍历其孩子节点,成对入队
                queue.offer(currLeft.right); // 内侧
                queue.offer(currRight.left);
                queue.offer(currLeft.left); // 外侧
                queue.offer(currRight.right);
            }
        }
        // 遍历结束,全部符合
        return true;
    }历结束,符合条件
            return true;
        }
    }

递归法,递归孩子节点后,注意对称是所有节点都对称,即 &&

递归时,可以利用 && 的短路性质进行提速,即将方式01修改为方式02

// 方式01
// 成对递归其孩子节点
boolean inner = assist(currLeft.right, currRight.left);
boolean outer = assist(currLeft.left, currRight.right);

return inner && outer; // 都对称才对称
// 方式02,会因为短路,不去计算后边的表达式
return assist(currLeft.right, currRight.left)&& assist(currLeft.left, currRight.right); // 都对称才对称
    // 递归法,三要素
    public boolean isSymmetric(TreeNode root) {
        // 处理空树
        if (root == null) {
            return true;
        } else {
            return assist(root.left, root.right);
        }
    }

    // 01.确定输入参数:待比较的当前节点 和 返回值:是否对称的累积结果
    // 02.终止条件:当前节点为空,此路径遍历结束,对称,返回 true
    // 03.单层操作:比较当前节点数值,对应递归孩子节点
    public boolean assist(TreeNode currLeft, TreeNode currRight) {
        // 终止条件:当前节点为空,此路径遍历结束,对称
        if ((currLeft == null) && (currRight == null)) {
            return true;
        } else if ((currLeft != null) && (currRight == null)) { // 单层操作
            return false;
        } else if ((currLeft == null) && (currRight != null)) {
            return false;
        } else if (currLeft.val != currRight.val) { // 此处当前节点都存在
            return false;
        } else { // 此处当前节点都存在,且数值相等
            // 成对递归其孩子节点
            boolean inner = assist(currLeft.right, currRight.left);
            boolean outer = assist(currLeft.left, currRight.right);

            return inner && outer; // 都对称才对称
        }
    }

100. 相同的树

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的,框架同上,对应的节点不同而已;


02.平衡二叉树

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1;

二叉树节点的深度:指从根节点到该节点的最长简单路径边的节点个数;

二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的节点个数,即当前节点的最大深度;

练习题:

110. 平衡二叉树

前序遍历 + 层序遍历的应用;前序遍历,判断每一个节点左右子树的高度差;层序遍历计算高度差;

注意后序遍历时间会大于前序遍历,推荐使用前序遍历;


03.完全二叉树节点的个数

已知满二叉树的层数为 n,则满二叉树节点的个数等于:

2 n − 1 2^n - 1 2n1

获取完全二叉树的高度,可以直接左孩子一条路走到头,即:currNode = currNode.left 进行更新;原因:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置;

利用完全二叉树的性质,如果当前节点的左子树高度和右子树相同,则左子树一定为满二叉树;

练习题:

222. 完全二叉树的节点个数

    // 递归法:
    // 01.确定输入参数:当前节点 和 返回值:左子树满二叉树 + 根节点的数量
    // 02.终止条件是当前节点为空
    // 03.单层操作,如果左右子树的深度相同,则左子树一定是满二叉树,否则左右子树都不是满二叉树,同时递归
    public int countNodes(TreeNode root) {
        if (root == null) {
            return 0;
        }

        // 单层操作,计算左右子树的高度
        int leftHeight = getHeight(root.left);
        int rightHeight = getHeight(root.right);
        if (leftHeight == rightHeight) { // 如果左子树是满二叉树
            // 计算左子树的节点个数 + 当前节点 = Math.pow(2, leftHeight) - 1 + 1
            int nodesNum = (int) Math.pow(2, leftHeight);
            // 还要继续递归右子树
            return nodesNum + countNodes(root.right);
        } else { // 左子树不满二叉树,左右孩子节点都需要递归
            // 注意加上当前节点本身的数量
            return 1 + countNodes(root.left) + countNodes(root.right);
        }
    }

    public int getHeight(TreeNode root) {
        int depth = 0;
        // 循环条件是当前节点非空
        while (root != null) {
            depth++; // 更新深度
            root = root.left; // 更新当前节点
        }
        return depth;
    }

04.二叉树的路径

前序遍历方便路径的生成;

练习题:

257. 二叉树的所有路径

迭代法,记录路径时,记录的是当前节点对应路径的字符串,这样就避免访问顺序和每次叶子节点都需要重新计算路径;

    // 迭代法,前、中、后序遍历都可,使用辅助栈,成对存取当前节点及其对应的路径
    // 注意:现存后取 和 强制转换
    public List<String> binaryTreePaths(TreeNode root) {
        // 创建存储结果的列表
        List<String> results = new LinkedList<>();
        // 如果根节点非空,则需要遍历路径
        if (root != null) {
            // 创建辅助栈
            Deque<Object> stack = new LinkedList<>();
            // 将根节点及其路径入栈
            stack.push(root);
            stack.push(String.valueOf(root.val));
            // 逐个遍历每个节点,循环条件是栈非空
            while (!stack.isEmpty()) {
                // 获取当前节点 及其 对应的路径,先存后取
                String currPath = (String) stack.pop();
                TreeNode currNode = (TreeNode) stack.pop();
                // 如果当前节点非空,则按顺序入栈,以前序为例,中左右出栈,则右左中入栈
                if (currNode != null) {
                    // 将非空节点入栈
                    if (currNode.right != null) {
                        stack.push(currNode.right);
                        stack.push(currPath + "->" + currNode.right.val);
                    }
                    if (currNode.left != null) {
                        stack.push(currNode.left);
                        stack.push(currPath + "->" + currNode.left.val);
                    }
                    stack.push(currNode);
                    stack.push(currPath);
                    stack.push(null);
                    stack.push(null);
                } else { // 如果当前节点为空,则处理
                    currPath = (String) stack.pop();
                    currNode = (TreeNode) stack.pop();
                    // 如果是叶子节点,则记录完整路径
                    if ((currNode.left == null) && (currNode.right == null)) {
                        results.add(currPath);
                    } 
                }
            }
        } 

        return results;
    }

递归法,使用参数传递当前节点对应的路径字符串;

    // 递归法,三要素
    public List<String> binaryTreePaths(TreeNode root) {
        // 创建记录结果的列表
        List<String> paths = new LinkedList<>();
        // 处理非空树
        if (root != null) {
            String path = String.valueOf(root.val);
            assist(root, path, paths);
        }

        return paths;
    }

    // 01.确定输入参数:当前节点、当前节点对应的路径字符串、记录所有路径的列表 和 返回值:无
    // 02.终止条件:当前节点为叶子节点,记录此条路径
    // 03.单层操作:递归时,需要判断节点非空
    public void assist(TreeNode currNode, String currPath, List<String> paths) {
        // 终止条件:当前节点为叶子节点,记录此条路径
        if ((currNode.left == null) && (currNode.right == null)) {
            paths.add(currPath);
            return; // 结束方法
        }

        // 单层操作
        if (currNode.left != null) {
            assist(currNode.left, currPath + "->" + currNode.left.val, paths);
        }
        if (currNode.right != null) {
            assist(currNode.right, currPath + "->" + currNode.right.val, paths);
        }
    }

05.二叉树的左叶子之和

左叶子之和是指二叉树中所有是左孩子的叶子节点的数值之和,即左叶子节点的判断条件是:01.当前节点的左孩子节点存在,02.该左孩子节点是否为叶子节点

练习题:

404. 左叶子之和

递归法,如果当前节点的左孩子是叶子节点就累积,否则当前值用0累积;

    // 递归法,所有的遍历都可,即在每个节点处理,判断当前节点的左孩子节点是否存在,且为左叶子节点
    // 以前序遍历为例,中左右
    // 01.确定输入参数:当前节点 和 返回值:所有左叶子节点的累积和
    // 02.终止条件是当前节点为空
    // 03.单层操作:判断当前节点的左孩子节点是否存在,是否为叶子节点,累积,递归其孩子节点
    public int sumOfLeftLeaves(TreeNode root) {
        // 终止条件
        if (root == null) {
            return 0;
        }

        // 单层操作
        int currLeftLeaf = 0;
        if ((root.left != null) && (root.left.left == null) && (root.left.right == null)) {
            currLeftLeaf += root.left.val;
        }
        // 递归孩子节点
        return currLeftLeaf + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
    }

迭代法,DFS的统一框架,处理位置,增加判断,如果当前节点的左孩子是叶子节点,则累积此左孩子的数值;

    // 迭代法,所有遍历均可,以中序遍历为例,辅助栈,模拟递归,左中右出栈,右中左入栈
    public int sumOfLeftLeaves(TreeNode root) {
        // 初始化所有左叶子之和
        int sum = 0;
        // 创建辅助栈
        Deque<TreeNode> stack = new LinkedList<>();
        // 如果根节点非空,将其入栈
        if (root != null) {
            stack.push(root);
        }
        // 逐个遍历节点,循环条件是栈非空
        while (!stack.isEmpty()) {
            // 获取当前节点
            TreeNode currNode = stack.pop();
            // 如果当前节点非空,则模拟递归,按“右中左”顺序入栈
            if (currNode != null) {
                // 将非空的孩子节点入栈
                if (currNode.right != null) {
                    stack.push(currNode.right);
                }
                stack.push(currNode);
                stack.push(null);
                if (currNode.left != null) {
                    stack.push(currNode.left);
                }
            } else { // 如果当前节点为空,则处理下一节点
                currNode = stack.pop();
                // 判断当前节点的左孩子节点是否为左叶子节点
                if ((currNode.left != null) && (currNode.left.left == null) && (currNode.left.right == null)) {
                    sum += currNode.left.val;
                }
            }
        }

        return sum;
    }

06.路径总和为目标值

左叶子之和是指二叉树中所有是左孩子的叶子节点的数值之和,即左叶子节点的判断条件是:01.当前节点的左孩子节点存在,02.该左孩子节点是否为叶子节点

练习题:

112. 路径总和

存在即找到一条路径所有节点数据之和为目标值即可;

113. 路径总和 II

找到所有的路径节点数据之和为目标值的所有路径,即需要遍历每个节点,记录结果时,注意深度拷贝,避免结果被其他过程修改;

递归法,终止条件是当前节点为叶子节点,需要特殊处理空树,并判断递归的孩子节点是否为空;

    // 创建记录符合条件路径的列表
    List<List<Integer>> paths = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        // 处理非空树
        if (root != null) {
            List<Integer> path = new LinkedList<>();
            path.add(root.val);
            assist(root, targetSum, path);
        }

        return paths;
    }

    // 递归法:
    // 01.确定输入参数:当前节点、当前目标、当前节点对应的路径 和 返回值:无
    // 02.当前节点为叶子节点,如果满足条件,记录当前路径
    // 03.单层操作,递归左右孩子节点
    public void assist(TreeNode currNode, int currTarget, List<Integer> path) {
        // 当前节点为叶子节点,如果满足条件,记录当前路径
        if ((currNode.left == null) && (currNode.right == null)) {
            // 如果符合条件,记录当前路径
            if (currNode.val == currTarget) {
                paths.add(new LinkedList<>(path));
            }
            return;
        }

        // 单层操作,当前节点为非叶子节点,递归非空的孩子节点,需要判断非空
        if (currNode.left != null) {
            path.add(currNode.left.val);
            assist(currNode.left, currTarget - currNode.val, path);
            path.remove(path.size() - 1); // 注意回溯
        }
        if (currNode.right != null) {
            path.add(currNode.right.val);
            assist(currNode.right, currTarget - currNode.val, path);
            path.remove(path.size() - 1); // 注意回溯
        }
    }

迭代法,使用辅助栈记录多个内容,泛型设置为 Object,注意先存后取和获取后强制转换;

    // 迭代法,DFS的统一框架,层序遍历的框架也可以,输出所有路径总和等于目标
    // 辅助栈需要记录:当前节点、当前节点对应的路径数值列表、当前目标值
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> paths = new LinkedList<>();
        if (root == null) {
            return paths;
        }

        Deque<Object> stack = new LinkedList<>();
        List<Integer> path = new LinkedList<>();
        path.add(root.val);
        stack.push(root);
        stack.push(path);
        stack.push(targetSum);

        while (!stack.isEmpty()) {
            // 注意先存后取
            Integer currTarget = (Integer) stack.pop();
            List<Integer> currPath = (LinkedList<Integer>) stack.pop();
            TreeNode currNode = (TreeNode) stack.pop();
            if (currNode != null) { // 按顺序入栈,前序中左右,入栈右左中
                if (currNode.right != null) {
                    stack.push(currNode.right);
                    currPath.add(currNode.right.val);
                    stack.push(new LinkedList<>(currPath));
                    currPath.remove(currPath.size() - 1); // 回溯
                    stack.push(currTarget - currNode.val);
                }
                if (currNode.left != null) {
                    stack.push(currNode.left);
                    currPath.add(currNode.left.val);
                    stack.push(new LinkedList<>(currPath));
                    currPath.remove(currPath.size() - 1); // 回溯
                    stack.push(currTarget - currNode.val);
                }
                stack.push(currNode);
                stack.push(currPath);
                stack.push(currTarget);
                stack.push(null);
                stack.push(null);
                stack.push(null);
            } else {
                currTarget = (Integer) stack.pop();
                currPath = (LinkedList<Integer>) stack.pop();
                currNode = (TreeNode) stack.pop();
                if ((currNode.val == currTarget) && (currNode.left == null) && (currNode.right == null)) {
                    paths.add(currPath);
                }
            }
        }

        return paths;
    }

07.二叉树的最近公共祖先

最近公共祖先的定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先),即最接近节点p、q;

练习题:

236. 二叉树的最近公共祖先

后序遍历的框架,先递归左右孩子,根据左右孩子递归值的情况,确定当前节点的返回值;

递归法,推荐的方式,利用后序遍历与回溯过程一致,利用回溯向上返回递归值,这个递归值就是最近公共祖先节点;

    // 递归法,推荐的方式,基于后续遍历的框架,自底向上的过程与回溯一致
    // 01.确定输入参数:当前节点、目标节点 和 返回值:最近公共祖先节点
    // 02.终止条件:当前节点为空(到底部)或为目标节点,开始回溯,返回 当前节点
    // 03.单层操作,当前节点一定非空,先递归左右孩子节点,再根据递归值进行返回
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if ((root == null) || (root == p) || (root == q)) {
            return root;
        }

        // 单层操作,后序遍历,左右中
        TreeNode leftSubTree = lowestCommonAncestor(root.left, p, q);
        TreeNode rightSubTree = lowestCommonAncestor(root.right, p, q);
        // 中,处理操作
        if ((leftSubTree == null) && (rightSubTree == null)) {
            // 如果当前节点的左右子树都不存在目标节点,则返回 null
            return null;
        } else if ((leftSubTree != null) && (rightSubTree == null)) {
            // 如果目标节点在左子树,则返回左子树的递归值
            return leftSubTree;
        } else if ((leftSubTree == null) && (rightSubTree != null)) {
            // 如果目标节点在右子树,则返回右子树的递归值
            return rightSubTree;
        } else {
            // 如果目标节点分别在左子树和右子树,则当前节点就是最近公共祖先节点,自下向上的第一个公共节点
            return root;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值