剑指Offer_#55 - II_平衡二叉树(LeetCode#110)

剑指Offer_#55 - II_平衡二叉树(LeetCode#110)

Contents

题目

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例 1:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7
返回 true 。

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

       1
      / \
     2   2
    / \
   3   3
  / \
 4   4
返回 false 。

限制:
1 <= 树的结点个数 <= 10000

思路分析

这一题是有两种方法,一种是普通的递归(自顶向下),另一种是分治法(提前阻断,自底向上)。说实话之前对于递归分治分的不是很清楚,我感觉所谓的分治法写出来不还是递归吗?到底区别在哪里呢?通过对比这道题的两种解法,就能理解清楚了。

递归 vs 分治

联系: 分治算法往往使用递归的代码实现的。
区别: 但是分治和递归不是一个概念,分治是一种算法思想,递归是一种实现方法(即自己调用自己)。分治算法的代码往往具有提前阻断的特性。

解答

方法1:普通递归(自顶向下)

算法流程

类似前序遍历

  1. 先处理当前节点,即判断当前节点的左右子树高度差距是否不超过1。
    • 这里又涉及到深度的计算,需要写一个辅助函数depth()
  2. 然后处理左右子节点,分别判断左右子节点是否满足上述条件。
    这个写法的问题在于depth()方法计算高度的时候会重复访问同一个节点。因为这是自顶向下的写法,所以无法利用子树的高度计算当前节点的高度。

解答1:普通递归(自顶向下)

class Solution {
    //前序遍历递归:先判断当前节点是否平衡,然后判断左右子树是否平衡
    public boolean isBalanced(TreeNode root) {
        if(root == null) return true;
        return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }
    //辅助函数:计算每个子树的最大深度,也是一个递归函数
    private int depth(TreeNode root){
        if(root == null) return 0;
        return Math.max(depth(root.left), depth(root.right)) + 1;
    }
}

复杂度分析

时间复杂度:O(nlogn),因为每次调用depth会带来O(logn)的复杂度
空间复杂度:O(n)

方法2:分治法(提前阻断,自底向上)

判断一棵树是否是平衡树,也是一个递归问题,因为需要递归地判断每个节点的左右子树深度是否相差不超过1,即需要判断每个节点是否是平衡的。
原问题与子问题结构相似,符合分治思想的特征,所以用分治法解决。

分治法模板

// 伪代码
public class Solution {
    public ResultType traversal(TreeNode root) {
        if (root == null) {
            // do something and return
        }

        // Divide
        ResultType left = traversal(root.left);
        ResultType right = traversal(root.right);

        // Conquer
        ResultType result = Merge from left and right

        return result;
    }
}

一般来说,
分治法代码会把递归调用的结果(也就是小问题的答案)保存到局部变量中,这个步骤叫做分(Divide)
这里就可以解释什么叫做提前阻断,因为这里的递归调用

        ResultType left = traversal(root.left);
        ResultType right = traversal(root.right);

会把代码阻塞在这个位置,直到满足终止条件,即遇到null,才会继续运行下面的代码。
接下来将利用保存下来的局部变量去解决更大的问题(上一层递归函数),这个步骤叫做治(Conquer)
第一次进行的过程是在最后一级递归调用,然后逐级回溯到第一级递归调用,这个过程是自底向上的。

特殊地,在本题当中的原问题和小问题又是什么?

  • 原问题:整棵树是否是平衡二叉树?
  • 小问题:根节点的左右子树是否是平衡二叉树?

根据这个思路就可以写出代码了。写出的代码依然是递归形式。

算法设计

根据问题的特点,我们采用后续遍历(更准确的说是分治法),先访问左右子树,再访问根节点,这样可以一边访问树的节点,一边判断当前的节点是否是平衡的。
递归函数isBalancedHelper返回值的含义:

  • -1表示此节点不平衡,返回false
  • 不是-1,则代表当前节点的深度,从叶节点开始,这个深度的返回值可以用于递推其父节点的深度,避免第二次遍历同一个节点。

解答2:分治法(提前阻断,自底向上)

class Solution {
    public boolean isBalanced(TreeNode root) {
        return isBalancedHelper(root) != -1;
    }

    private int isBalancedHelper(TreeNode root){
        if(root == null) return 0;
        int left = isBalancedHelper(root.left);
        //如果left/right是-1,就不需要计算diff了,因为两个都是-1的时候,diff是0,可能就误判了
        if(left == -1) return -1;
        int right = isBalancedHelper(root.right);
        if(right == -1) return -1;
        int diff = Math.abs(left - right);
        return diff <= 1 ? Math.max(left,right) + 1:-1;
    }
}

复杂度分析

时间复杂度:O(n)
空间复杂度:O(n)

参考

posted @ 2020-07-21 16:01  Howfar's  阅读(167)  评论(0编辑  收藏  举报