Balanced Binary Tree
Interview Prep
The problem
A binary tree is height-balanced if, at every node, the heights of the two subtrees differ by at most one. Return whether the input tree is height-balanced. Empty trees are balanced.
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right Naive: O(n²)
The straightforward implementation runs depth
independently at every node, then recurses. Each depth
call is O(size) and we do it at every node — quadratic
total. Same trap as Diameter.
def is_balanced_naive(root):
"""O(n^2): depth recomputed at every node."""
if root is None: return True
def depth(n):
if n is None: return 0
return 1 + max(depth(n.left), depth(n.right))
if abs(depth(root.left) - depth(root.right)) > 1: return False
return is_balanced_naive(root.left) and is_balanced_naive(root.right) Pattern: fold the check into one post-order pass
Standard tree-DP trick: have the recursion return the depth of the
current subtree, or a sentinel value (here -1)
meaning "this subtree was already found to be unbalanced". Once
either child returns the sentinel, propagate it up immediately
without computing anything else.
This is "early exit through the return value" — a clean way to short-circuit a recursive check without raising exceptions or threading a boolean around. The same idiom shows up in many tree-validation problems (e.g., Validate BST with bounds).
Solution
def is_balanced(root: Node | None) -> bool:
"""O(n). Post-order returns depth, or -1 as a sentinel for 'unbalanced below here'."""
UNBALANCED = -1
def check(node):
if node is None:
return 0
l = check(node.left)
if l == UNBALANCED: return UNBALANCED
r = check(node.right)
if r == UNBALANCED: return UNBALANCED
if abs(l - r) > 1: return UNBALANCED
return 1 + max(l, r)
return check(root) != UNBALANCED Trace
Tree: 1
/ \
2 2
/ \
3 3
/ \
4 4
check(leaf 4): returns 1.
check(3, left): l=1, r=0, |1-0|=1 ok, returns 2.
check(3, right): returns 1.
check(2, left): l=check(3,left)=2, r=check(3,right)=1, |2-1|=1 ok, returns 3.
check(2, right): returns 1.
check(1):
l = check(2, left) = 3.
r = check(2, right) = 1.
|3 - 1| = 2 > 1 -> return UNBALANCED.
is_balanced(root) -> False Complexity
- Time:
O(n)with the sentinel approach. - Space:
O(h)recursion stack.
Variants of "balanced"
The problem says "height-balanced" — the AVL-tree definition. Other definitions exist:
- Weight-balanced: at every node, the sizes (not heights) of left and right subtrees differ by at most some factor. Used by weight-balanced trees and treaps.
- Red-black balanced: a more relaxed structural
invariant via node colors. Allows up to 2× height ratio while
preserving
O(log n)operations. - Perfect / complete: stricter notions used in heap layouts. A complete tree has every level full except possibly the last, which is left-justified.