Day15--二叉树--222. 完全二叉树的节点个数,110. 平衡二叉树,257. 二叉树的所有路径,404. 左叶子之和

发布于:2025-08-01 ⋅ 阅读:(16) ⋅ 点赞:(0)

Day15–二叉树–222. 完全二叉树的节点个数,110. 平衡二叉树,257. 二叉树的所有路径,404. 左叶子之和

本篇文章,基本上每道题目都给出了2-3篇题解。每道题写之前思考几个问题:

  1. 是前序遍历还是后序遍历?
  2. 是递归还是迭代好?
  3. 是先判断null再进入递归好,还是先进入递归后判断null好?

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

思路参考自:完全二叉树的节点数,你真的会算吗?

参考二:《代码随想录》:

完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。

对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。

对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢?

在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。

思路:

完全二叉树,它的左右子树,其中至少有一颗是满二叉树。满二叉树直接根据公式计算,另一颗按照普通二叉树计算。

//  完全二叉树,它的左右子树,其中至少有一颗是满二叉树。
//  满二叉树直接根据公式计算,另一颗按照普通二叉树计算。
//  对于“不是满二叉树”的那颗子树,往下遍历的时候,也可能会有“满二叉树”的情况
//  本代码优化的速度就在于,凡是“满二叉树”就套公式。
class Solution {
    public int countNodes(TreeNode root) {
        // 其实这种就一句话的函数,是可以直接写在调用方法里的。
        // 多写一个方法是因为,这里的节点叫root,容易误解。叫node容易理解。
        return getNodeNum(root);
    }

    private int getNodeNum(TreeNode node) {
        // 基本上所有代码就是为了“判断是否是满二叉树”
        if(node==null){
            return 0;
        }
        // 分左右子树
        TreeNode left = node.left;
        TreeNode right = node.right;

        // 记录左右子树的高度
        // 初始化为1是因为,判断左右子树是否高度相等,是在cur本节点判断的
        // 如果成立,证明从本节点开始就是满二叉树,要加上本层
        int leftHeight = 1;
        int rightHeight = 1;

        // 统计高度
        while (left != null) {
            left = left.left;
            leftHeight++;
        }
        while (right != null) {
            right = right.right;
            rightHeight++;
        }

        // 如果左右子树高度相同,是一颗满二叉树,可以直接根据公式计算
        if (leftHeight == rightHeight) {
            return (int) Math.pow(2, leftHeight) - 1;
        }

        // 如果不是满二叉树,则按照普通二叉树的逻辑计算
        return countNodes(node.left) + countNodes(node.right) + 1;
    }
}

思路:

递归法:完全当成普通二叉树来算

class Solution {
    public int countNodes(TreeNode root) {
        return getNodesNum(root);
    }

    private int getNodesNum(TreeNode node) {
        // 节点为空,返回0
        if (node == null) {
            return 0;
        }
        // 递归左子树
        int leftNum = getNodesNum(node.left);
        // 递归右子树
        int rightNum = getNodesNum(node.right);
        // 子树节点+1(自身)->返回给上一层
        int curNum = leftNum + rightNum + 1;
        return curNum;
    }
}

110. 平衡二叉树

《代码随想录》:使用前序求深度,使用后序求高度。

思路:

  • 后序遍历法:自底向上统计
  • 如果高度差大于1,返回-1做标记给上层
  • 每一层算高度的时候:取左右子树之中的max,然后+1,当做是本层的高度(因为高度是自底向上算的)
//  后序遍历法:自底向上统计
class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) == -1 ? false : true;
    }

    private int getHeight(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int leftHeight = getHeight(node.left);
        int rightHeight = getHeight(node.right);
        // 如果下层返回的有-1,直接返回-1给上层
        if (leftHeight == -1 || rightHeight == -1) {
            return -1;
        }
        // 如果高度差大于1,返回-1做标记给上层
        if (Math.abs(leftHeight - rightHeight) > 1) {
            return -1;
        }
        // 取左右子树之中的max,然后+1,当做是本层的高度(因为高度是自底向上算的)
        return Math.max(leftHeight, rightHeight) + 1;
    }
}

257. 二叉树的所有路径

  • 方法一

思路:

后序遍历:自下而上传递字符串,把孩子传上的字符串拼接本层,加工后传给上一层

// 后序遍历:自下而上传递字符串,把孩子传上的字符串拼接本层,加工后传给上一层
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        return getChildrenString(root);
    }

    private List<String> getChildrenString(TreeNode node) {
        List<String> list = new ArrayList<>();
        if (node == null) {
            return list;
        }
        // 拼接左节点传上来的字符串
        if (node.left != null) {
            List<String> leftList = getChildrenString(node.left);
            // 每一个字符串,都要加上本节点的val和"->"
            // 然后重新加到本节点的list里,每一个node都会新一个list
            for (String s : leftList) {
                String temp = node.val + "->" + s;
                list.add(temp);
            }
        }
        // 拼接右节点传上来的字符串
        if (node.right != null) {
            List<String> rightList = getChildrenString(node.right);
            for (String s : rightList) {
                String temp = node.val + "->" + s;
                list.add(temp);
            }
        }
        // 如果是叶子节点,直接返回本节点的val的字符串形式
        if (node.left == null && node.right == null) {
            list.add("" + node.val);
        }
        return list;
    }
}

记录:自己写的,每一次调用都要传List<String>而且还要for循环拼接,消耗大。

  • 方法二

思路:

优化后,每层的操作只是拼一次字符串

前序遍历,自顶向下传递本层拼好的字符串,到叶子节点就“结算”,添加进结果集

// 前序遍历,自顶向下传递本层拼好的字符串,到叶子节点就“结算”,添加进结果集
class Solution {

    public List<String> binaryTreePaths(TreeNode root) {
        // 结果集,要作为参数传递
        List<String> res = new ArrayList<>();

        getPaths(root, "", res);
        return res;
    }

    public void getPaths(TreeNode node, String s, List<String> res) {
        // 处理current本节点
        if (node == null) {
            return;
        }
        // 当是叶子节点的时候,“结算”
        if (node.left == null && node.right == null) {
            res.add(s + node.val);
            return;
        }
        // 非叶子节点,加上本层val加"->"传递给下一层
        String ss = new StringBuilder(s).append(node.val).append("->").toString();
        // 下一层
        getPaths(node.left, ss, res);
        getPaths(node.right, ss, res);
    }
}
  • 方法三

思路:

这个方法,其实也可以再加一个List<Integer>参数,每层只传递数字,到叶子节点,再一次性拼接字符串。

优点:结果的时候再用StringBuilder一次性拼接,开销小。

// 前序遍历,自顶向下传递路径上的节点的paths列表,到叶子节点就“结算”拼接字符串,添加进结果集
class Solution {

    public List<String> binaryTreePaths(TreeNode root) {
        // 结果集
        List<String> res = new ArrayList<>();
        // 当前路径上的节点值
        List<Integer> paths = new ArrayList<>();
        // 开始遍历
        getPaths(root, paths, res);
        return res;
    }

    public void getPaths(TreeNode node, List<Integer> paths, List<String> res) {
        // 递归终止条件:如果当前节点为null,直接返回(空节点不处理)
        if (node == null) {
            return;
        }
        
        // 前序遍历:先处理当前节点,将其值加入路径
        paths.add(node.val);
        
        // 当是叶子节点的时候,“结算”
        if (node.left == null && node.right == null) {
            // 使用StringBuilder效率更高
            StringBuilder sb = new StringBuilder();
            // 这里不遍历最后一个节点,最后一个节点不加"->"
            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());
        } else {
            // 非叶子节点:递归遍历左子树
            getPaths(node.left, paths, res);
            // 递归遍历右子树
            getPaths(node.right, paths, res);
        }
        
        // 回溯操作:无论当前节点是否为叶子节点,
        // 在处理完所有子节点后,都要将当前节点从路径中移除
        // 保证回溯到父节点时,路径信息正确
        paths.remove(paths.size() - 1);
    }
}
  • 方法四

引自《代码随想录》

思路:

迭代法,前序遍历

特点:Stack中的泛型是<Object>,把它当成了“方法调用的栈来用”,其实就是方法调用时候的“形参列表”

//  迭代法,前序遍历
//  特点:Stack中的泛型是<Object>,把它当成了“方法调用的栈来用”,其实就是方法调用时候的“形参列表”
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        Deque<Object> stack = new ArrayDeque<>();
        // 节点和路径同时入栈
        stack.push(root);
        stack.push(root.val + "");
        while (!stack.isEmpty()) {
            // 节点和路径同时出栈
            String paths = (String) stack.pop();
            TreeNode node = (TreeNode) stack.pop();
            // 若找到叶子节点
            if (node.left == null && node.right == null) {
                res.add(paths);
            }
            //右子节点不为空
            if (node.right != null) {
                stack.push(node.right);
                stack.push(paths + "->" + node.right.val);
            }
            //左子节点不为空
            if (node.left != null) {
                stack.push(node.left);
                stack.push(paths + "->" + node.left.val);
            }
        }
        return res;
    }
}

404. 左叶子之和

《代码随想录》:判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。

思路:

递归遍历。要保证node.left是个叶子,才要它的值。

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        return getLeftSum(root);
    }

    private int getLeftSum(TreeNode node) {
        int leftNodeSum = 0;
        int rightNodeSum = 0;
        if (node.right != null) {
            rightNodeSum = getLeftSum(node.right);
        }
        if (node.left != null) {
            leftNodeSum = getLeftSum(node.left);
            // 要保证node.left是个叶子,才要它的值
            if (node.left.left == null && node.left.right == null) {
                return leftNodeSum + rightNodeSum + node.left.val;
            }
        }
        return leftNodeSum + rightNodeSum;
    }
}

以下代码引自《代码随想录》

思路:

思考一下,先判断null再进入,还是进入之后再判断null?其实这就是两份代码的不同之处。

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;
        if (root.left != null && root.left.left == null && root.left.right == null) { 
            midValue = root.left.val;
        }
        int sum = midValue + leftValue + rightValue;  // 中
        return sum;
    }
}

思路:

迭代法。

如果是先判断null再入栈,可以ArrayDeque;如果有null值入栈,只能LinkedList

//  迭代法:判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if (root == null) {
            return 0;
        }
        // 如果是先判断null再入栈,可以ArrayDeque;如果有null值入栈,只能LinkedList
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.add(root);
        int sum = 0;
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            // 当本节点的左节点不为空,左节点没有任何子节点,才算是左叶子。
            if (node.left != null && node.left.left == null && node.left.right == null) {
                sum += node.left.val;
            }
            if (node.right != null) {
                stack.add(node.right);
            }
            if (node.left != null) {
                stack.add(node.left);
            }
        }
        return sum;
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到