title: Balanced Binary Tree slug: balanced-binary-tree difficulty: easy leetcode_id: 110 leetcode_url: https://leetcode.com/problems/balanced-binary-tree/ categories: - trees - recursion patterns: - dfs - tree-traversal function_signature: "def is_balanced(root: TreeNode) -> bool:" test_cases: visible: - input: { root: [3, 9, 20, null, null, 15, 7] } expected: true - input: { root: [1, 2, 2, 3, 3, null, null, 4, 4] } expected: false - input: { root: [] } expected: true hidden: - input: { root: [1] } expected: true - input: { root: [1, 2, null, 3] } expected: false - input: { root: [1, null, 2, null, 3] } expected: false - input: { root: [1, 2, 3, 4, 5, 6, 7] } expected: true - input: { root: [1, 2, 2, 3, null, null, 3, 4, null, null, 4] } expected: false description: | Given a binary tree, determine if it is **height-balanced**. A **height-balanced** binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one. constraints: | - The number of nodes in the tree is in the range `[0, 5000]` - `-10^4 <= Node.val <= 10^4` examples: - input: "root = [3,9,20,null,null,15,7]" output: "true" explanation: "The left subtree has height 1 (just node 9), and the right subtree has height 2 (node 20 with children 15, 7). At every node, the height difference between subtrees is at most 1." - input: "root = [1,2,2,3,3,null,null,4,4]" output: "false" explanation: "The left subtree of the root has height 3, while the right subtree has height 1. The difference is 2, which exceeds the allowed difference of 1." - input: "root = []" output: "true" explanation: "An empty tree is considered balanced by definition." explanation: intuition: | Think of a balanced tree like a well-organised bookshelf where no section is dramatically taller than its neighbours. The key insight is that a tree is balanced if and only if **every single node** in the tree has balanced subtrees. It's not enough to just check the root — you need to verify this property recursively at every node. Imagine you're a building inspector checking floor heights. You start at the top floor (root) and work your way down. At each floor, you measure the height of the left wing and the right wing. If the difference is more than one floor, the building fails inspection immediately. You continue checking every sub-section until you've verified the entire structure. The clever optimisation is to combine the height calculation with the balance check. Instead of calculating heights separately and then checking balance (which would be redundant work), we can propagate a "failure signal" up the tree if any subtree is unbalanced. approach: | We solve this using a **Bottom-Up DFS** approach that combines height calculation with balance checking: **Step 1: Define the recursive helper function** - Create a function `check_height(node)` that returns the height of the subtree if balanced, or `-1` if unbalanced - This dual-purpose return value eliminates the need for separate balance checks   **Step 2: Handle base case** - If the node is `None`, return `0` (an empty tree has height 0 and is balanced)   **Step 3: Recursively check left subtree** - Call `check_height(node.left)` to get the left subtree's height - If the result is `-1`, the left subtree is unbalanced — propagate `-1` upward immediately   **Step 4: Recursively check right subtree** - Call `check_height(node.right)` to get the right subtree's height - If the result is `-1`, the right subtree is unbalanced — propagate `-1` upward immediately   **Step 5: Check balance at current node** - Calculate `abs(left_height - right_height)` - If the difference exceeds `1`, return `-1` to signal imbalance - Otherwise, return `max(left_height, right_height) + 1` as the height of this subtree   **Step 6: Return the final result** - Call `check_height(root)` and return `True` if the result is not `-1`, otherwise `False` common_pitfalls: - title: Top-Down Redundant Computation description: | A naive approach calculates the height of each subtree at every node independently: ```python def is_balanced(root): if not root: return True left_height = height(root.left) right_height = height(root.right) if abs(left_height - right_height) > 1: return False return is_balanced(root.left) and is_balanced(root.right) ``` This results in **O(n^2) time complexity** for skewed trees because `height()` is called repeatedly on the same nodes. For a tree with 5000 nodes, this causes significant performance degradation. wrong_approach: "Calculate height separately at each node" correct_approach: "Combine height calculation with balance checking in a single pass" - title: Forgetting to Check All Nodes description: | Some solutions only check the balance condition at the root node. A tree can have balanced immediate children but have deeply unbalanced subtrees further down. For example, a root with left child having a long left-skewed subtree and right child being a single node — the root's children might look balanced locally, but the overall tree isn't. The recursive approach naturally handles this by checking every node. wrong_approach: "Only check the root node's children" correct_approach: "Recursively verify balance at every node in the tree" - title: Incorrect Height Definition description: | Be careful with the definition of height. The height of a node is the number of edges on the longest path from that node to a leaf. An empty tree has height 0 (or -1 in some definitions), and a single node has height 0 (or 1). Using inconsistent definitions will cause off-by-one errors in the balance check. wrong_approach: "Mix up height definitions between edges and nodes" correct_approach: "Use consistent height definition: empty tree = 0, leaf node = 1" key_takeaways: - "**Bottom-up recursion**: When you need to check a property at every node AND compute something (like height), combine both operations in one traversal" - "**Early termination with sentinel values**: Using `-1` to signal failure allows the algorithm to short-circuit and avoid unnecessary computation" - "**Foundation for tree problems**: This pattern of returning either a valid value or a failure signal is common in many tree problems (e.g., validating BST, finding LCA)" - "**Time-space tradeoff**: The O(n) solution uses O(h) stack space where h is the tree height, which is optimal for this problem" time_complexity: "O(n). Each node is visited exactly once, and we perform O(1) work at each node." space_complexity: "O(h) where h is the height of the tree. This is the recursion stack space, which is O(log n) for a balanced tree and O(n) for a skewed tree." solutions: - approach_name: Bottom-Up DFS is_optimal: true code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_balanced(root: TreeNode | None) -> bool: def check_height(node: TreeNode | None) -> int: # Base case: empty tree has height 0 and is balanced if not node: return 0 # Check left subtree - propagate failure immediately left_height = check_height(node.left) if left_height == -1: return -1 # Check right subtree - propagate failure immediately right_height = check_height(node.right) if right_height == -1: return -1 # Check balance at current node if abs(left_height - right_height) > 1: return -1 # Signal that tree is unbalanced # Return height of this subtree return max(left_height, right_height) + 1 # Tree is balanced if check_height doesn't return -1 return check_height(root) != -1 explanation: | **Time Complexity:** O(n) — Each node is visited exactly once. **Space Complexity:** O(h) — Recursion stack where h is tree height. This bottom-up approach combines height calculation with balance verification. By returning `-1` as a sentinel value for unbalanced subtrees, we achieve early termination and avoid redundant computation. - approach_name: Top-Down (Naive) is_optimal: false code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_balanced(root: TreeNode | None) -> bool: def height(node: TreeNode | None) -> int: # Calculate height of subtree if not node: return 0 return max(height(node.left), height(node.right)) + 1 # Base case: empty tree is balanced if not root: return True # Check balance at current node left_height = height(root.left) right_height = height(root.right) if abs(left_height - right_height) > 1: return False # Recursively check both subtrees return is_balanced(root.left) and is_balanced(root.right) explanation: | **Time Complexity:** O(n^2) — For each node, we calculate heights which visits all descendants. **Space Complexity:** O(h) — Recursion stack where h is tree height. This naive approach calculates height separately at each node, leading to redundant traversals. For a skewed tree of n nodes, we perform approximately n + (n-1) + (n-2) + ... + 1 = O(n^2) operations. Included here to illustrate why the bottom-up approach is preferred.