A binary tree is height-balanced when every node's left and right subtrees differ in height by at most 1. The trick is to compute height and detect imbalance in one post-order DFS pass, propagating a -1 sentinel the moment any subtree is off — no redundant traversals.
Given the root of a binary tree, return true if the tree is height-balanced: for every node, the heights of its left and right subtrees differ by at most 1.
Worked example. Tree 3,9,20,null,null,15,7 (level-order):
3
/ \
9 20
/ \
15 7Node 3: left height = 1, right height = 2, difference = 1 ✓. Both subtrees rooted at 20 have height 1. Every node passes → return true.
Contrast with 1,2,2,3,3,null,null,4,4: the leftmost path reaches depth 4 while the right side only reaches depth 2 → return false.
-1 as a sentinel instead of the real height. Every ancestor that sees -1 immediately propagates it upward — no second traversal, no global flag. One pass, O(n) total work.null node returns 0 — an empty tree has height zero and is trivially balanced.dfs(node.left). If it returns -1, short-circuit and return -1 immediately — no point checking the right subtree.dfs(node.right). Same fast-path on -1.|leftH − rightH| > 1, return -1.1 + Math.max(leftH, rightH). This value is what the parent will receive.isBalanced returns dfs(root) !== -1.isBalanced calls height(node.left) and height(node.right) per node, then recurses into both children. That recomputes heights from scratch for every node, giving O(n²) worst-case on a skewed tree. The sentinel trick merges both jobs into one DFS pass.The implicit call stack uses O(h) space: O(log n) for a balanced tree, O(n) for a fully skewed one. An iterative post-order with an explicit stack avoids stack-overflow risk on very deep inputs but adds implementation complexity.
-1as an "unbalanced" sentinel.1▶function isBalanced(root: TreeNode | null): boolean {2▶ function dfs(node: TreeNode | null): number {3 if (node === null) return 0; // base case: empty tree has height 045 const leftH = dfs(node.left);6 if (leftH === -1) return -1; // fast-path: left already unbalanced78 const rightH = dfs(node.right);9 if (rightH === -1) return -1; // fast-path: right already unbalanced1011 if (Math.abs(leftH - rightH) > 1) return -1; // this node is unbalanced1213 return 1 + Math.max(leftH, rightH); // balanced; return true height14 }1516 return dfs(root) !== -1;17}
function isBalanced(root: TreeNode | null): boolean {
function dfs(node: TreeNode | null): number {
if (node === null) return 0; // base case: empty tree has height 0
const leftH = dfs(node.left);
if (leftH === -1) return -1; // fast-path: left already unbalanced
const rightH = dfs(node.right);
if (rightH === -1) return -1; // fast-path: right already unbalanced
if (Math.abs(leftH - rightH) > 1) return -1; // this node is unbalanced
return 1 + Math.max(leftH, rightH); // balanced; return true height
}
return dfs(root) !== -1;
}null node contributes height 0 and is always balanced. Returning 0 (not −1) lets the parent count correctly.-1, the subtree is already unbalanced — short-circuit immediately. This is what makes the overall pass O(n): once we see the sentinel we never visit more nodes on that branch.|leftH − rightH| > 1. This is the core predicate; returning -1 here propagates the failure upward through all ancestors.1 + Math.max(leftH, rightH) is the standard recursive height formula. The parent will use this value in its own balance check.dfs(root) !== -1 to convert the height/sentinel result to a boolean.process.setMaxCallStackSize in Node. In an interview, mention the risk and offer the iterative alternative.-1, return the node itself (or its value) when unbalanced, and a sentinel object for "balanced with height h".maxDiameter variable in the outer scope; update it with leftH + rightH at each node while still returning the true height upward.height() function for every node inside isBalanced. Each height() call walks a subtree, so nodes near the root are visited O(n) times each.function isBalanced(root: TreeNode | null): boolean {
function dfs(node: TreeNode | null): number {
if (node === null) return 0; // base case: empty tree has height 0
const leftH = dfs(node.left);
if (leftH === -1) return -1; // fast-path: left already unbalanced
const rightH = dfs(node.right);
if (rightH === -1) return -1; // fast-path: right already unbalanced
if (Math.abs(leftH - rightH) > 1) return -1; // this node is unbalanced
return 1 + Math.max(leftH, rightH); // balanced; return true height
}
return dfs(root) !== -1;
}At each node, independently compute the height of the left and right subtrees with a helper, check that they differ by at most one, then recurse into both children — the literal translation of the definition before you notice the heights can be reused.
function isBalanced(root: TreeNode | null): boolean {
function height(node: TreeNode | null): number {
if (node === null) return 0;
return 1 + Math.max(height(node.left), height(node.right));
}
if (root === null) return true;
if (Math.abs(height(root.left) - height(root.right)) > 1) return false;
return isBalanced(root.left) && isBalanced(root.right);
}heightwalks each subtree from scratch on every visit, so a skewed tree does O(n) work at each of O(n) nodes — O(n²) overall. Returning -1 from a single bottom-up pass reuses the heights you already computed and brings it down to O(n).| "is the tree balanced / height-balanced" | post-order DFS returning height or −1 sentinel |
| need height AND a validity check in one pass | merge both into the return value: real height OR sentinel |
| recursive result that short-circuits on bad subtree | check sentinel immediately after each recursive call |
| "diameter / max path sum" (same shape) | track side-effect in outer variable; return height upward |
function isBalanced(root: TreeNode | null): boolean {
function dfs(node: TreeNode | null): number {
if (node === null) return 0;
const L = dfs(node.left);
if (L === -1) return -1;
const R = dfs(node.right);
if (R === -1) return -1;
if (Math.abs(L - R) > 1) return -1;
return 1 + Math.max(L, R);
}
return dfs(root) !== -1;
}-1 if any subtree rooted at or below the current node is unbalanced.3,9,20,null,null,15,7, what does isBalanced return?