02_二叉树的性质

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;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值