zoukankan      html  css  js  c++  java
  • 剑指Offer_#55

    剑指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)

    参考

  • 相关阅读:
    loadrunner 字符集与检查点的探讨
    oracle使用游标进行循环数据插入
    QTP之WinMenu对象的测试
    VBS数组深入浅出
    Loadrunner 如何访问数据库
    loadrunner字符串处理函数
    动态SQL现实一个表中求多列的和
    oracle中WMSYS.WM_CONCAT函数的版本差异
    强制卸载LINUX系统自带JDK
    loadrunner调用QTP脚本来实现性能测试(一次现场特殊需求的解决方案)
  • 原文地址:https://www.cnblogs.com/Howfars/p/13355617.html
Copyright © 2011-2022 走看看