|
| 1 | +# LeetCode 第 110 号问题:平衡二叉树 |
| 2 | + |
| 3 | +> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。 |
| 4 | +> |
| 5 | +> 同步博客:https://www.algomooc.com |
| 6 | +
|
| 7 | +题目来源于 LeetCode 上第 110 号问题:平衡二叉树。 |
| 8 | + |
| 9 | +### 题目描述 |
| 10 | + |
| 11 | +给定一个二叉树,判断它是否是高度平衡的二叉树。 |
| 12 | + |
| 13 | +本题中,一棵高度平衡二叉树定义为: |
| 14 | + |
| 15 | +> 一个二叉树*每个节点* 的左右两个子树的高度差的绝对值不超过1。 |
| 16 | +
|
| 17 | +**示例 1:** |
| 18 | + |
| 19 | +``` |
| 20 | + 3 |
| 21 | + / \ |
| 22 | + 9 20 |
| 23 | + / \ |
| 24 | + 15 7 |
| 25 | +``` |
| 26 | + |
| 27 | +返回 `true` 。 |
| 28 | + |
| 29 | +**示例 2:** |
| 30 | + |
| 31 | +给定二叉树 `[1,2,2,3,3,null,null,4,4]` |
| 32 | + |
| 33 | +``` |
| 34 | + 1 |
| 35 | + / \ |
| 36 | + 2 2 |
| 37 | + / \ |
| 38 | + 3 3 |
| 39 | + / \ |
| 40 | + 4 4 |
| 41 | +``` |
| 42 | + |
| 43 | +返回 `false` 。 |
| 44 | + |
| 45 | +### 题目解析 - 自顶向下 |
| 46 | + |
| 47 | +这道题可以算是递归的充分使用了, 每一个子树都是子问题. |
| 48 | + |
| 49 | +根据题意, 直观的想法就是计算当前节点左右子树的高度差了, 具体算法流程如下: |
| 50 | + |
| 51 | +*定义* 方法 `depth(root)` 计算 root 最大高度 |
| 52 | + |
| 53 | +- **终止条件:** 当 `root` 为空,即越过叶子节点,则返回高度 0 |
| 54 | +- **返回值:** Max(左子树高度, 右子树高度 ) + 1 |
| 55 | + |
| 56 | +*定义* 方法 `isBalanced(root)` 判断树 `root` 是否平衡 |
| 57 | + |
| 58 | +- **特例处理:** 若树根节点 `root` 为空,则直接返回 true |
| 59 | +- **返回值:** 所有子树都需要满足平衡树性质,因此以下三者使用与 逻辑与 连接 |
| 60 | + - `abs(depth(root.left) - depth(root.right)) < 2` :判断 **当前子树** 是否是平衡树 |
| 61 | + - `isBalanced(root.left)` : 先序遍历递归,判断 **当前子树的左子树** 是否是平衡树; |
| 62 | + - `isBalanced(root.right)` : 先序遍历递归,判断 **当前子树的右子树** 是否是平衡树; |
| 63 | + |
| 64 | +> 通过流程能发现, 暴力法虽然容易想到, 但是会产生大量冗余计算, 因此时间复杂度也就会高; |
| 65 | +> |
| 66 | +> 想避免这种情况, 移步向下看 自底向上 方法 |
| 67 | +
|
| 68 | +### 动画描述 |
| 69 | + |
| 70 | +<img src="../Animation/Animation1.gif" alt="Animation1" style="zoom:150%;" /> |
| 71 | + |
| 72 | +### 参考代码 |
| 73 | + |
| 74 | +```javascript |
| 75 | +/** |
| 76 | + * JavaScript 描述 |
| 77 | + * 自顶向下递归 |
| 78 | + */ |
| 79 | +function depth(root) { |
| 80 | + if (root == null) { |
| 81 | + return 0; |
| 82 | + } |
| 83 | + return Math.max(depth(root.left), depth(root.right)) + 1; |
| 84 | +}; |
| 85 | +var isBalanced = function(root) { |
| 86 | + if (root == null) { |
| 87 | + return true; |
| 88 | + } |
| 89 | + return Math.abs(depth(root.left) - depth(root.right)) < 2 && |
| 90 | + isBalanced(root.left) && |
| 91 | + isBalanced(root.right); |
| 92 | +}; |
| 93 | +``` |
| 94 | + |
| 95 | +### 复杂度分析 |
| 96 | + |
| 97 | +- 时间复杂度: **O(Nlog_2 N)** |
| 98 | + |
| 99 | + 最差情况下, isBalanced(root) 遍历树所有节点,占用 O(N)O(N) ;判断每个节点的最大高度 depth(root) 需要遍历 各子树的所有节点 ,子树的节点数的复杂度为 O(log_2 N) |
| 100 | + |
| 101 | +- 空间复杂度: **O(N)** |
| 102 | + |
| 103 | + 最差情况下(树退化为链表时),系统递归需要使用 O(N) 的栈空间 |
| 104 | + |
| 105 | +### 题目解析 - 自底向上 |
| 106 | + |
| 107 | +**自顶向下** 计算 `depth` 存在大量冗余, 每次调用 `depth` 时,要同时计算其子树高度。 |
| 108 | + |
| 109 | +**自底向上** 计算每个子树的高度只会计算一次。先递归计算当前节点的子节点高度,然后再通过子节点高度判断当前节点是否平衡,从而消除冗余。 |
| 110 | + |
| 111 | +**自底向上** 与 **自顶向下** 的逻辑相反,首先判断子树是否平衡,然后比较子树高度判断父节点是否平衡。算法如下: |
| 112 | + |
| 113 | +*定义* 方法 `recur(root):` : 判断子树是否平衡 | 返回当前节点高度 |
| 114 | + |
| 115 | +- **递归终止条件:** |
| 116 | + - 当越过叶子节点时, 返回高度 0 |
| 117 | + - 当左(右)子树高度 `left== -1` 时,代表此子树的 **左(右)子树** 不是平衡树, 因此直接返回 `-1` |
| 118 | +- **递归返回值:** |
| 119 | + - 当节点 `root` 左 / 右子树的高度差 < 2:返回以节点 root 为根节点的子树的最大高度Max( left, right ) + 1 |
| 120 | + - 当节点 `root` 左 / 右子树的高度差 >= 2 :则返回 `-1` , 代表 **此子树不是平衡树** |
| 121 | + |
| 122 | +*定义* 方法 `isBalanced(root)` : 判断当前树是否平衡 |
| 123 | + |
| 124 | +- **返回值:** 若 `recur(root) != 1` , 则说明此树平衡, 返回 `true` , 否则返回 `false` |
| 125 | + |
| 126 | +### 动画描述 |
| 127 | + |
| 128 | +<img src="../Animation/Animation2.gif" alt="Animation2" style="zoom:150%;" /> |
| 129 | + |
| 130 | +### 参考代码 |
| 131 | + |
| 132 | +```javascript |
| 133 | +/** |
| 134 | + * JavaScript 描述 |
| 135 | + * 自底向上递归 |
| 136 | + */ |
| 137 | +function recur(root) { |
| 138 | + if (root == null) { |
| 139 | + return 0; |
| 140 | + } |
| 141 | + let leftHeight = recur(root.left); |
| 142 | + if (leftHeight == -1) { |
| 143 | + return -1; |
| 144 | + } |
| 145 | + let rightHeight = recur(root.right); |
| 146 | + if (rightHeight == -1) { |
| 147 | + return -1; |
| 148 | + } |
| 149 | + return Math.abs(leftHeight - rightHeight) < 2 ? |
| 150 | + Math.max(leftHeight,rightHeight) + 1 : -1; |
| 151 | +}; |
| 152 | +var isBalanced = function(root) { |
| 153 | + return recur(root) != -1; |
| 154 | +}; |
| 155 | +``` |
| 156 | + |
| 157 | +### 复杂度分析 |
| 158 | + |
| 159 | +- 时间复杂度 **O(N)**: N为树的节点数;最差情况下,需要递归遍历树的所有节点。 |
| 160 | +- 空间复杂度 **O(N)**: 最差情况下(树退化为链表时),系统递归需要使用 O(N) 的栈空间。 |
| 161 | + |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | + |
0 commit comments