平衡二叉树:每个节点都量一次高度差
不是「整棵看起来高不高」,而是每一个节点左右两边的高度差都不能超过 1。自底向上一次带回高度,歪了就传 -1。
定义:空树平衡;对每个节点,左子树与右子树的高度差绝对值 ≤ 1,且左右子树本身也平衡。后序递归返回两个信息:当前子树高度、是否平衡。任一处失衡即返回 false。时间 O(n),空间 O(h)。
给你一棵二叉树的根,判断它是不是高度平衡的。平衡只有一句话:树上每一个节点,左右子树高度差的绝对值都不超过 1。高度用大白话说,就是从空往上数有几层:空树高度是 0,一个叶子高度是 1,任意节点的高度等于左右里较大的那个再加 1。
主例层序 [3, 9, 20, null, null, 15, 7]:根 3,左叶子 9,右子树是 20,20 下面再挂 15 和 7。根左边高度 1、右边高度 2,差正好是 1;20 的左右都是叶子,差是 0。每个节点都过关,返回 true。
只看根的左右差不够——根可能还行,某一棵子树内部已经歪了。本文要回答:为什么不能先写一个算高度的函数再对每个节点重算一遍,以及怎样让同一次返回值既报告高度、又在失败时传 -1。
平衡钉在每一个节点上,不是只看根
一棵树平衡,当且仅当:对每个节点,左高度减右高度的绝对值不超过 1,并且左右子树自己也平衡。空树没有节点可破规则,算平衡。叶子左右都是空,差是 0,也平衡。定义是递归的,所以检查必须下沉到所有子树,不能只站在根上量一次。
只看根会漏。主例根差 1,看起来没问题。如果把 20 的右边再往下挂一长串,根的左右差可能还是 1,但 20 那个节点已经歪了。题目说的是每个节点,不是「整体最大深度差」或「根看起来还行」。
第一直觉几乎总是拆成两个函数:一个专门算高度,一个对每个节点取左右高度看差,再递归问左右平不平衡。主例能给出 true,左链反例也能给出 false。它缺的不是对错,而是同一段路走了太多次。算根 3 的高度时,已经把 9、20、15、7 都走了一遍;再单独检查 20,又要把 15 和 7 再走一遍。最坏一条链时,每个祖先都把下面整段重算,时间掉到平方。
全局「每个节点」意味着检查不能只看根。高度从空算起是 0,不要和节点个数混成一件事。本题只判断,不要求把树转成平衡树,也不做 AVL 旋转。
自底向上:一次返回高度,歪了就传 -1
暴力缺的不是定义,而是一次遍历里同时交出两样东西:这个子树有多高,以及它内部有没有已经不平衡。函数只返回高度,调用方还得再问一次内部平不平衡——又要往下走。函数只返回是否平衡,调用方又不知道左右到底差几层,没法判断当前节点自己过不过关。
约定:合法高度最小是 0,-1 不可能是真高度,拿来当失败信号不会和真实层数打架。每个节点先问完左右孩子,再做三件事。左边已经返回 -1,自己也返回 -1,不必再讨论当前这一层的差;右边同理。两边都是真高度,但绝对值差大于 1,自己返回 -1。两边都合法且差不超过 1,自己的高度是左右里较大的那个加 1。
这正是后序、自底向上:先把孩子问完,再给自己下结论。孩子已经算过的高度,父节点直接用;孩子已经发现的失败,父节点直接向上传。发现 -1 要立刻往上传,因为题目只要一个布尔值,子树已经歪了,祖先再高也救不回来。
用手走主例。空节点高度记 0。叶子 15:左右都是 0,差 0,高度 1。叶子 7 同样是 1。节点 20:左 1、右 1,差 0,高度 2。叶子 9:高度 1。根 3:左 1、右 2,差 1,不超过 1,高度 3。没有人返回 -1,整棵树平衡。演示场景从 15 走到 20 再走到根,标出的正是这张「节点 → 高度」表。
对照一棵左链 1→2→3。叶子 3 高度 1。节点 2:左 1、右 0,差 1,高度 2。根 1:左 2、右 0,差 2,返回 -1。注意节点 2 本身是平衡的,坏在根 1。如果只检查叶子,会误判。把链再加深一档,节点 2 会先返回 -1,根 1 收到左边的 -1,立刻也返回 -1,不会再幻想「根也许左右还行」。
Go:返回 -1 表示失衡
func isBalanced(root *TreeNode) bool {return height(root) != -1}func height(n *TreeNode) int {if n == nil { return 0 }l, r := height(n.Left), height(n.Right)if l == -1 || r == -1 || abs(l-r) > 1 { return -1 }return 1 + max(l, r)}
1height 的返回值有两层含义:非负是真高度,-1 是这棵子树已经失衡。isBalanced 只看根拿到的是不是 -1。
2空树返回 0,叶子会变成 1。不要把空树的 0 和失败哨兵 -1 混用。
3左或右已经是 -1,或者当前差大于 1,立刻把 -1 交上去。主例走不到这条;左链在根上走这条。
4否则返回 1+max(l,r)。主例里 20 返回 2,根返回 3。每个节点进出一次,高度不再被祖先重复索取。
总结
每个节点左右高度差都不能超过 1;一次后序带回高度,歪了就传 -1。主例根差 1,整棵 true。
- 平衡是每个节点的性质。只看根,子树内部再歪一截就会漏。
- 先写 maxDepth 再对每个节点调用两次,答案可能对,时间最坏平方。高度和平衡必须叠在同一次返回值里。
- -1 是失败哨兵,不是高度公式里的空树。发现不平衡还返回 max+1,父节点会把假高度当成真的。