110 · Balanced Binary Tree
The sentinel-abort idiom. One recursion returns two different kinds of thing
through the same int: a real height, or −1 meaning
"stop asking, this tree is already unbalanced." That single overloaded return is what turns an
O(n log n) solution into an O(n) one.
1 — The problem
A tree is height-balanced when every node has left and right subtree heights differing by at most 1. The obvious reading of that sentence produces the obvious — and slow — solution:
| Approach | Shape | Time |
|---|---|---|
| Check every node | At each node call height(left) and height(right), compare, then recurse into both children and repeat. | O(n log n) balanced, O(n²) degenerate — height re-walks the same subtrees once per ancestor. |
| Sentinel abort | One postorder pass. height returns the real height, or -1 if a violation has already been found anywhere below. | O(n) — every node is entered at most once, and often not at all. |
The trick is recognising that the two questions — "how tall is this subtree?" and "is it balanced?" — can share one return channel, because the answer to the second is only ever needed as a veto. As soon as a veto exists, the height is irrelevant, so the height slot is free to carry the veto instead.
Sub-variant C solves the same tension by splitting the two answers apart: return one value, record the other in a field. Here they are compressed into a single integer. Both are worth having; C generalises further, this one is shorter.
2 — Watching −1 propagate
This tree is unbalanced at node 2: its left subtree is 2 tall and its right side is empty. Watch what happens to node 9 — the entire right half of the tree — once that violation is found.
Node 9 is never entered. The root calls left, receives −1 on line 4, and returns before line 5 is ever reached. That is not an optimisation bolted on afterwards — it falls out of putting the check between the two recursive calls rather than after both of them. Move line 4 below line 5 and the code is still correct, but it now walks the whole tree.
2.1 Why −1 is a safe sentinel
An overloaded return only works if the sentinel can never be produced legitimately. Heights
here are counted in nodes, so a real height is always ≥ 0, and
-1 is unreachable by the normal path. Two things follow:
- Do not switch to edge-counting in this problem. If
nullreturns-1so that leaves return 0, the sentinel collides with a real height and the recursion starts aborting on empty subtrees. - The propagation checks are mandatory, not decorative. Drop line 4 and a
−1 flows into
Math.abs(L - R)as if it were a height, where it looks like a very short subtree — and can silently produce a balanced-looking answer.
When no such spare value exists — say the aggregate is an arbitrary int
that could genuinely be −1 — you have outgrown this idiom. Return a small object, or
use the record/return split of sub-variant C.
3 — Complexity and edge cases
- Time O(n), space O(h). Each node returns once and the sentinel short-circuits whole subtrees, so the true count is often well under n.
- Empty tree: balanced —
height(null)is 0, never −1, and the wrapper reports true. - Single node: balanced.
|0 - 0| = 0. - A node with one child that is a leaf is the boundary case:
|1 - 0| = 1, which is allowed. Add one more level below that child and it becomes 2, which is not. If your code disagrees on that pair, the comparison is> 1and not≥ 1. - Common bug: checking balance only at the root. A tree can have equal overall left and right heights and still be badly unbalanced two levels down — the definition is quantified over every node, which is exactly why the check lives inside the recursion rather than outside it.
4 — Reference implementation
Java 21Sentinel abort, matching the visualizer.12 lines
public boolean isBalanced(TreeNode root) {
return height(root) != -1;
}
private int height(TreeNode nd) {
if (nd == null) return 0;
int L = height(nd.left);
if (L == -1) return -1; // abort before touching the right subtree
int R = height(nd.right);
if (R == -1) return -1;
if (Math.abs(L - R) > 1) return -1;
return 1 + Math.max(L, R); // identical to LC 104
}Strip the three sentinel lines and what is left is LC 104 verbatim. That is the point of sub-variant A: the aggregation skeleton does not change, only what it is allowed to refuse.