https://leetcode-cn.com/problems/balanced-binary-tree/
题意
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
题解
解法一:自顶向下
这道题希望我们判断一棵树是否是平衡二叉树,即所有节点的左右子树高度差不能超过1。既然判断的条件是高度差不能超过1,那么我们势必要算出左右子树的高度,那么如何求一个节点的高度?当前节点的高度与左右子树有关,那么可以使用递归方法求解,如下。
//获取高度
private int height(TreeNode root){
if(root==null)return 0;
return 1+Math.max(height(root.left),height(root.right));
}
要判断一棵树是否是平衡二叉树,那么我们只要对每一个节点判断是否平衡即可,至此自然就想到了遍历。通过遍历判断每一个节点是否平衡,不平衡就停止遍历输出false这就是解题思路。那么使用哪一种遍历呢?其实使用哪一种遍历都是一样的,因为我们对每一个节点都需要进行判断,哪里可能先失衡是难以预料的。
class Solution {
public boolean isBalanced(TreeNode root) {
if(root==null)return true;
return isBalanced(root.left)&&isBalanced(root.right)&&Math.abs(height(root.left)-height(root.right))<2;
}
//获取高度
private int height(TreeNode root){
if(root==null)return 0;
return 1+Math.max(height(root.left),height(root.right));
}
}
事实上,这种方法存在着大量的重复计算,而问题就在于height方法对于高度的获取。假如我们使用先序遍历对树进行访问,那么如下所示,当我们访问节点1,想要判断它是否失衡时,我们必须获取它的左子树与右子树的高度。
于是获取节点2与节点3的高度。但节点2与3的高度获取,同样也取决于他们的左右子树。
于是再获取左右子树高度。
然而还是同样的问题,还是要先获取子树高度。
此时除了根节点1之外的节点高度都已经被计算过了,终于可以判断头节点是否失衡,但发现没有失衡,于是按先序遍历的顺序进入左节点判断。
此时判断节点2是否失衡,还是要获取节点4、5的高度。
节点4高度的获取还是取决于其左右节点8、9的高度,于是计算8、9的高度。
此时终于可以判断节点2是否失衡,但相信大家都发现了,之前为了判断根节点1是否失衡,将所有子节点的高度都计算了一遍,而在后续子节点的判断中,仍然需要计算大量之前已经计算过的节点,也就是存在着大量的重复计算,假想一下这是一棵非常庞大的树,那么这样重复的计算也是非常可怕的。或许将之前的遍历改为后续遍历可以修正这个问题?但哪怕改为后续遍历,也只是先判断节点2和3,然后再判断节点1,而高度的计算是在height方法中并没有改变,为了判断节点2所计算过的子节点,在节点1中还是会重复计算。
解法二:记忆化
既然方法一中需要重复计算之前已经计算过的节点,造成了重复计算,那么我们能不能将已经计算过的节点高度存起来,当需要再次使用时我们不进行计算,而是直接使用之前计算的结果呢?显然是可以的,但问题是如何存储每一个节点的高度?也是需要有一个办法将每一个节点区分开。在此我们可以回想一下完全二叉树的性质,当我们将根节点编号编号为1时,其他节点的编号如下图所示。
虽然题目中的树并不是完全二叉树,但我们仍然可以使用这个方法给每个节点编号,这样不同的节点就可以得到不同的编号了(左节点的编号是父节点的两倍,右节点的编号是父节点的两倍加一),然后我们再将每个计算过的节点高度存入HashMap中,需要使用时再取出即可。
class Solution {
HashMap<Integer,Integer> map=new HashMap<>();
public boolean isBalanced(TreeNode root) {
return traversal(root,1);
}
private boolean traversal(TreeNode root,int num){
if(root==null)return true;
return traversal(root.left,num*2)&&traversal(root.right,num*2+1)&&Math.abs(height(root.left,num*2)-height(root.right,num*2+1))<2;
}
private int height(TreeNode node,int num){
if(node==null)return 0;
if(map.containsKey(num))
return map.get(num);
int l=height(node.left,num*2);
int r=height(node.right,num*2+1);
map.put(num,1+Math.max(l,r));
return map.get(num);
}
}
解法三:自底向下
事实上,我们在调用height方法获取节点高度时,已经对其所有子节点进行了一次遍历,计算节点高度,并将每个子节点的高度返回供父节点使用,只不过height方法仅仅计算了高度,却没有判断每个节点是否平衡,所以才画蛇添足又进行了一次遍历来进行判断,因此我们需要对此进行修改,而height方法的返回值是int类型,如果一个节点失衡了,我们可以返回一个-1来代表失衡,并提前结束遍历。
class Solution {
public boolean isBalanced(TreeNode root) {
return getHeight(root)!=-1;
}
private int getHeight(TreeNode root){
if(null==root)return 0;
int leftHeight=getHeight(root.left);
int rightHeight=getHeight(root.right);
if(leftHeight==-1||rightHeight==-1||Math.abs(leftHeight-rightHeight)>1)
return -1;
return 1+Math.max(leftHeight,rightHeight);
}
}
让我们简单看看这段代码进行了什么操作。
后续遍历的顺序,先计算节点8的高度为1,然后返回给父节点4,并进入右节点。
计算节点9的高度,然后返回给父节点4,再计算父节点。
有了两个子节点高度,父节点的高度以及是否失衡也就可以知道了。
此外还可以对代码进行优化,上面的代码先判断了左右节点是否失衡,然后再判断自身,但其实当左节点失衡的时候,已经没有必要再计算右节点了,应该直接返回-1,优化代码如下。
class Solution {
public boolean isBalanced(TreeNode root) {
return getHeight(root)!=-1;
}
private int getHeight(TreeNode root){
if(null==root)return 0;
int leftHeight,rightHeight;
if((leftHeight=getHeight(root.left))==-1
||(rightHeight=getHeight(root.right))==-1
||Math.abs(leftHeight-rightHeight)>1)
return -1;
return 1+Math.max(leftHeight,rightHeight);
}
}