Balanced Binary Tree
Given the root of a binary tree, return true if the tree is height-balanced. Height-balanced means that at every node in the tree, the heights of its left and right subtrees differ by at most 1 — not just at the root. An empty tree is balanced, so a null root returns true. You return a boolean only, not the height.
- The number of nodes in the tree is in the range [0, 5000]
- -10⁴ <= Node.val <= 10⁴
- The balance condition must hold at every node, not only at the root
- An empty subtree has height 0
root = [3,9,20,null,null,15,7]trueroot = [1,2,2,3,3,null,null,4,4]falseroot = [1,2,3,4,5,6,null,8]trueroot = []trueThe definition is doing more work than it first appears. "The two subtrees differ in height by at most 1" is not a statement about the root — it is a statement that has to hold at every single node. A tree can look perfectly even at the top and still be disqualified by some lopsided node buried five levels down, so the answer is a giant AND across all nodes: the tree is balanced only if nobody fails.
Write the definition out literally and you get a working solution immediately. We already know how to measure a subtree's height from Maximum Depth, so: a node is fine if the two heights it sees differ by at most 1, and its left subtree is fine, and its right subtree is fine.
def height(node):
if node is None: return 0
return 1 + max(height(node.left), height(node.right))
def isBalanced(node):
if node is None: return True
if abs(height(node.left) - height(node.right)) > 1: return False
return isBalanced(node.left) and isBalanced(node.right)Correct, and wasteful in a way worth measuring rather than waving at. Every call to height walks an entire subtree, so checking the root costs a full O(N) sweep. Then we recurse and check the root's left child — which sweeps that subtree all over again, even though the sweep we just finished had already computed its height and thrown the number away. Every node ends up being touched once for each of its ancestors, so the total work is the sum of all subtree sizes, which is roughly N × H. On a balanced tree H is about log₂N and this is tolerable; on a chain-shaped tree H is N and it degrades to O(N²) — around 25 million node-visits at the 5000-node limit, for a question that should need 5000.
Name the waste precisely, because the fix is its exact opposite: we compute each height twice over — once inside a parent's measurement, and again when that subtree's own turn comes — and discard the answer in between.
The two recursions have identical shapes. height descends the whole tree; isBalanced descends the whole tree. They are the same walk, run twice, one nested inside the other. So merge them: perform a single post-order walk in which each node returns its height to its parent and has already verified balance for everything beneath it.
That works because of the order in which facts become available. When a node receives both children's heights, it holds — at that exact moment, for free — everything needed to test its own condition. There is no reason to schedule a second visit later; the check belongs right there, in the middle of the height computation.
The remaining problem is the return channel. The parent needs a height, but it also needs to hear about a failure discovered deep below. Rather than return a pair, use a value that cannot be a genuine height as a failure signal: real heights are 0 or more, so -1 is free to mean "something under here is broken." A node returns its true height if all is well, or -1 if it or anything below it violated the rule.
def check(node):
if node is None:
return 0 # empty subtree: height 0, trivially fine
left = check(node.left)
if left == -1: return -1 # failure below — stop measuring, just report it
right = check(node.right)
if right == -1: return -1
if abs(left - right) > 1:
return -1 # this node is the violator
return 1 + max(left, right) # fine: report the real height upward
return check(root) != -1Also note the early returns. The moment a child reports -1, the answer is already decided, so we skip the sibling entirely — this is genuine short-circuiting, not just tidiness, and on a badly broken tree it can end the whole traversal near the first violation.
Follow what happened at the left 2 carefully: it passed its own test and reported honestly. Balance is not violated by any single node being deep — only by a node whose two sides disagree by too much. The root was the first place that comparison went wrong.
Every node is now entered exactly once and does constant work — two calls, two comparisons, one max — so time drops from O(N × H) to O(N). Extra space stays at O(H) for the recursion stack, unchanged. We paid nothing at all for this speedup; we simply stopped throwing away numbers we had already computed.
That is the reflex worth building: when a solution calls a helper that re-walks the same structure the outer recursion is already walking, fuse them. The test is easy to apply — ask whether the information the helper recomputes was available, in the outer walk, at the moment it was needed. Here it was, exactly at the post-order combine step. Diameter of Binary Tree is the same fusion again with a slightly different twist: the quantity we want (the longest bend through a node) is not the quantity the recursion must return (the height), so instead of a sentinel we keep a running maximum on the side.
Balance Calculus
"A single -1 return bubbles up the failure immediately (Early Exit)."
Strategy
We calculate height and check balance in one pass. If a subtree is unbalanced, we return -1 instead of height.
"Height = max(L, R) + 1"