Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
A tree is height-balanced if, at EVERY node, the heights of the two subtrees differ by at most 1. The naïve solution recomputes height at every node — O(n²). The interview-grade answer is a single bottom-up (postorder) DFS that returns each subtree's height, but returns a -1 sentinel the moment it detects imbalance, short-circuiting the rest. That's O(n) time, O(h) space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you spot that the naïve top-down approach recomputes heights repeatedly (O(n²)) and can refactor to a single bottom-up pass that returns height while detecting imbalance.
Where candidates slip
Only checking balance at the root, or writing the O(n²) version that calls a separate height() function at every node, or forgetting to propagate the -1 sentinel so a deep imbalance gets lost.
How to stand out
Lead with the naïve O(n²), explain exactly where the repeated work is, then present the -1-sentinel bottom-up O(n) solution and articulate the 'fuse compute + validate' principle.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewIs every node height-balanced?Compute height how often?Re-call height() at each nodeOnce, bottom-upExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Top-Down — Recompute Height at Every Node (Brute)
1Hint 1Explore hintHide hint
Start from the definition: a tree is balanced if its root's two subtree heights differ by ≤ 1 AND both subtrees are themselves balanced. How would you turn that directly into code?
2Hint 2Explore hintHide hint
Write a height() helper, then an isBalanced() that calls height(left), height(right), checks the difference, and recurses into both children.
3Hint 3Explore hintHide hint
Now count the work: height() walks an entire subtree, and you call it at every node — that's the same nodes visited over and over.
A · Core idea
The most literal translation of the definition: a tree is balanced if (a) its root's two subtree heights differ by at most 1, and (b) both subtrees are recursively balanced.
Why it's correct. It mirrors the definition exactly, including the crucial 'every node' part — by recursing isBalanced into both children, it re-checks the height-difference condition at each node, so a deep imbalance can't slip past.
Where the waste is. The height() helper walks an entire subtree to compute one number. We call it at every node, so the nodes near the bottom of the tree get re-traversed once for each ancestor above them. On a skewed tree of n nodes, that's roughly n + (n-1) + ... + 1 = O(n²) total work. The information we need — each subtree's height — is computed over and over instead of once.
The cue to optimize. Whenever you see a helper that recomputes the same aggregate that the outer recursion is already traversing, that's a signal to fuse them: compute the height on the way back up and validate balance at the same time. That refactor is the optimal solution.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
root = [1,2,2,3,3,null,null,4,4]isBalanced(root=1)
calls height(left)=3, height(right)=1
|3-1|=2 > 1 → already false at rootfalse (but via O(n²) repeated traversals)C · Code with teaching comments
Reference implementation preview// Definition for a binary tree node. class TreeNode { int val; TreeNode left; TreeNode right;Explore the full codeHide the full code
D · Why it works and complexityO(n²) timeO(h) spaceOpen the invariant and proofExplore the proofHide the proof
The redundancy is structural: height() and isBalanced() traverse the same nodes, just at different times. Any time two recursions cover identical ground, fold them into one — compute the value and check the condition in a single bottom-up pass.
ComplexityO(n²) time, O(h) space — isBalanced() is called at each of the n nodes, and each call invokes height() which itself is O(size of subtree). Summed over all nodes this is O(n²) in the worst case (a skewed tree). Space is O(h) for the recursion.
E · Edge cases and pitfalls3Only checking balance at the root and skipping the recursion into children — misses deep imbalances.Explore edge casesHide edge cases
Pitfalls
- Only checking balance at the root and skipping the recursion into children — misses deep imbalances.
- Believing it's efficient because each individual function looks linear — the composition is quadratic.
- Not recognizing the repeated traversal as the thing to optimize.
Approach 2 of 2 · Optimal
Bottom-Up Height with -1 Sentinel (Optimal)
1Hint 1Explore hintHide hint
The naïve version recomputes height everywhere. What if a single recursion returned the height AND signaled imbalance at the same time?
2Hint 2Explore hintHide hint
Have the helper return the subtree's height normally, but return a special value like -1 the moment it discovers any imbalance below it.
3Hint 3Explore hintHide hint
At each node: get left and right heights; if either is -1, or they differ by more than 1, return -1. Otherwise return 1 + max(left, right). The root is balanced iff the helper doesn't return -1.
A · Core idea
Fuse the two jobs the naïve version did separately. A single postorder DFS computes each subtree's height and validates the balance condition during the same walk, using a sentinel to carry a failure verdict upward.
The sentinel idea. The helper normally returns a non-negative height. We overload it: return -1 to mean 'a violation was found somewhere in this subtree'. Since a real height can never be negative, -1 is an unambiguous failure flag that rides up the recursion for free — no separate boolean or exception needed.
The per-node logic. Compute the left height; if it's -1, the left subtree is already unbalanced, so return -1 immediately (short-circuit — don't even look right). Do the same for the right. If both children are fine, check |leftHeight - rightHeight| > 1: if so, this node is the imbalance, return -1. Otherwise the subtree rooted here is balanced, so return its genuine height 1 + max(left, right) for the parent to use.
Why it's O(n). Each node is touched once and does O(1) work, because the height it needs from its children is returned by the recursion rather than recomputed by a fresh traversal. The quadratic blow-up of the naïve version came entirely from re-deriving heights; computing them bottom-up exactly once removes it.
The transferable principle. This is the classic 'compute a value and validate a property in a single pass' refactor. The same move turns Diameter of a Binary Tree, Max Path Sum, and Count Univalue Subtrees from naïve double-recursions into clean single passes — the helper returns the aggregate the parent needs while a side-channel (a sentinel, an outer variable, or a tuple) carries the answer.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
root = [1,2,2,3,3,null,null,4,4] (left side two levels deeper)Interactive diagram
Heights bubbling up, root poisoned to -1
height(4) leavesThe left subtree reaches height 3, the right height 1; |3-1|=2 forces the root to return -1.
falseC · Code with teaching comments
D · Why it works and complexityO(n) timeO(h) spaceOpen the invariant and proofExplore the proofHide the proof
Overloading a numeric return with an impossible value (-1 for a quantity that's normally ≥ 0) is a tidy way to smuggle a boolean verdict through a recursion that's already returning a number. It avoids a second pass and a second data channel — one return value does double duty.
ComplexityO(n) time, O(h) space — Each node is visited exactly once: the height is computed on the way back up, and the balance check is O(1) per node → O(n) total time. Recursion depth is the tree height h → O(h) space. The -1 short-circuit can only make it faster, never slower.
E · Edge cases and pitfalls6root = [] — height(null) = 0, and 0 != -1 → returns true (empty tree is balanced).Explore edge casesHide edge cases
Edge cases handled
root = []height(null) = 0, and 0 != -1 → returns true (empty tree is balanced).root = [1]height = 1, not -1 → true.Perfectly skewed tree of 3+ nodesFirst node where the height difference exceeds 1 returns -1, which short-circuits all ancestors → false in one pass.
Pitfalls
- Forgetting to short-circuit on a child's -1 — you'd do extra work and could mis-handle the sentinel as a real height.
- Treating -1 as a valid height in the max/+1 arithmetic — always check for it before using the value.
- Using > vs >= incorrectly in the |left - right| test — the rule is 'differ by at most 1', so fail only when the difference exceeds 1.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic boolean isBalanced(TreeNode root) { return height(root) != -1; } private int height(TreeNode node) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Only checking the root's two subtrees
A balanced root can sit above a deeply unbalanced subtree. Balance must hold at EVERY node, so the recursion has to validate each one.
Explore the code comparisonHide the code comparison
Only checking the root's two subtrees
A balanced root can sit above a deeply unbalanced subtree. Balance must hold at EVERY node, so the recursion has to validate each one.
Don't write this
def isBalanced(self, root):
if not root:
return True
return abs(self.height(root.left) - self.height(root.right)) <= 1
# never checks the children's own balanceWrite this instead
def isBalanced(self, root):
def height(node):
if not node:
return 0
l = height(node.left)
if l == -1: return -1
r = height(node.right)
if r == -1: return -1
if abs(l - r) > 1: return -1
return 1 + max(l, r)
return height(root) != -12Quadratic recompute of heights
Calling a standalone height() at every node re-walks overlapping subtrees, giving O(n²). Compute the height once on the way up and check balance during the same pass.
Explore the code comparisonHide the code comparison
Quadratic recompute of heights
Calling a standalone height() at every node re-walks overlapping subtrees, giving O(n²). Compute the height once on the way up and check balance during the same pass.
Don't write this
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
return Math.abs(height(root.left) - height(root.right)) <= 1
&& isBalanced(root.left) && isBalanced(root.right); // height() re-run everywhere
}Write this instead
public boolean isBalanced(TreeNode root) {
return height(root) != -1;
}
private int height(TreeNode node) {
if (node == null) return 0;
int l = height(node.left); if (l == -1) return -1;
int r = height(node.right); if (r == -1) return -1;
if (Math.abs(l - r) > 1) return -1;
return 1 + Math.max(l, r);
}Revise
Carry the pattern into the interview
Pattern to remember
Bottom-up height DFS with a -1 sentinel for 'unbalanced'
Code pattern to memorize
height(node):
if node is null: return 0
L = height(node.left); if L == -1: return -1
R = height(node.right); if R == -1: return -1
if |L - R| > 1: return -1 # unbalanced → poison upward
return 1 + max(L, R)Rules to remember
- 1Balance is checked at every node, not just the root — a balanced root can still hide a deep imbalance below.
- 2Naïve = compute height at each node separately → O(n²). The fix is to compute height once, bottom-up.
- 3Use -1 as a sentinel meaning 'this subtree is already unbalanced' so the imbalance propagates up without extra flags.
- 4Short-circuit: the instant a child returns -1, return -1 immediately — don't keep recursing.
- 5Empty subtree height = 0; balance condition is
|leftHeight - rightHeight| ≤ 1.
Use this pattern when
- You must verify a per-node structural property that depends on subtree heights
- You want to compute a value and validate a condition in a single pass (fuse them)
- Avoiding repeated work that an O(n²) recompute-from-scratch would cause
Choose another pattern when
- You only need the height, with no balance check → plain Maximum Depth
- The condition is global ordering, not height → Validate BST's bounds DFS
- You need the actual unbalanced node, not a boolean → carry the node, not a sentinel
Interview takeaway
The lesson is fusing computation with validation. Instead of computing height and checking balance in two separate passes (O(n²)), do both in one postorder traversal and use a sentinel value to carry the failure verdict upward. That 'compute-and-check in one pass with a sentinel' trick recurs across tree problems.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternFuse computation with validation in one bottom-up (postorder) pass, using a sentinel value to carry a failure verdict upward. This converts naïve O(n²) 'recompute an aggregate at every node' solutions into O(n). The same refactor powers Diameter, Max Path Sum, and Count Univalue Subtrees.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Maximum Depth of Binary TreeThe plain height recursion this problem builds on — no balance check, just 1 + max(left, right).
- Diameter of Binary TreeSame bottom-up height pass, but track leftHeight + rightHeight to find the longest path through any node.
- Binary Tree Maximum Path SumHelper returns the best downward path sum; an outer variable captures the best through-node sum — same fuse pattern.
- Balance a Binary Search TreeComing soonInorder to a sorted array, then rebuild by recursively picking the middle as root.