110. Balanced Binary Tree
Problem Description
Given a binary tree, you need to determine whether it is height-balanced.
A height-balanced binary tree is defined as a binary tree in which the heights of the left and right subtrees of every node differ by at most 1.
In other words, for every node in the tree:
- Calculate the height of its left subtree
- Calculate the height of its right subtree
- The absolute difference between these two heights must be at most 1
- This condition must be satisfied for all nodes in the tree, not just the root
The height of a tree is the number of edges on the longest path from the root to a leaf node. An empty tree has height 0.
For example:
- A tree with nodes
[3,9,20,null,null,15,7]
forming a balanced structure would returntrue
- A tree with nodes
[1,2,2,3,3,null,null,4,4]
where the left subtree is much deeper than the right would returnfalse
The solution uses a clever optimization: instead of checking balance and calculating height separately for each node, it combines both operations. The helper function returns -1
to indicate an imbalanced subtree, which allows early termination without checking all nodes unnecessarily. If any subtree is unbalanced, the entire tree is unbalanced.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure.
DFS
- Yes: We arrive at DFS (Depth-First Search) as the recommended approach.
Conclusion: The flowchart suggests using DFS for this problem. This makes perfect sense because:
- We need to traverse the entire tree to check if every node satisfies the balance condition
- We need to calculate heights from the bottom up (leaves to root), which naturally fits a post-order DFS traversal
- DFS allows us to recursively compute the height of each subtree while simultaneously checking the balance condition
- The recursive nature of DFS matches the recursive structure of the tree itself
The solution implements DFS through the recursive height
function, which:
- Traverses to the leaf nodes first (base case returns 0 for null nodes)
- Calculates heights while returning back up the tree
- Propagates the "unbalanced" signal (
-1
) immediately when detected, allowing early termination
This is why DFS is the optimal pattern for the Balanced Binary Tree problem - it efficiently combines height calculation with balance checking in a single traversal.
Intuition
The naive approach would be to check each node individually: for every node, calculate the height of its left and right subtrees, then verify if their difference is at most 1. However, this leads to redundant calculations since we'd repeatedly compute heights for the same subtrees.
The key insight is that while calculating the height of a tree, we're already visiting every node. So why not check the balance condition during the same traversal?
Think about how we calculate height recursively:
- The height of a null node is 0
- The height of any node is
1 + max(left_height, right_height)
During this calculation, we already have both left_height
and right_height
available. This is the perfect moment to check if abs(left_height - right_height) <= 1
.
But here's the clever part: how do we signal that a subtree is unbalanced? We can't just return false
because the function needs to return a height. The solution is to use a sentinel value -1
to indicate "this subtree is unbalanced."
This creates an elegant propagation mechanism:
- If any subtree returns
-1
, we immediately know it's unbalanced - We can skip further calculations and propagate
-1
upward - This acts like an early termination signal that bubbles up to the root
So the function serves dual purposes:
- When it returns a non-negative number, that's the actual height
- When it returns
-1
, it signals the tree is unbalanced
This transforms an O(n²)
naive solution (checking balance at each node separately) into an O(n)
solution with a single post-order traversal.
Solution Approach
The solution implements a bottom-up recursive approach using DFS (post-order traversal). Here's how the implementation works:
We define a helper function height(root)
that calculates the height of a binary tree while simultaneously checking for balance:
Base Case:
- If
root
isNone
, return0
(an empty tree has height 0)
Recursive Case:
- Recursively calculate the height of the left subtree:
l = height(root.left)
- Recursively calculate the height of the right subtree:
r = height(root.right)
Balance Check: After getting both heights, we perform three checks:
- If
l == -1
: The left subtree is already unbalanced - If
r == -1
: The right subtree is already unbalanced - If
abs(l - r) > 1
: The current node violates the balance condition
If any of these conditions is true, we return -1
to signal that the tree is unbalanced.
Height Calculation:
If all checks pass, the current subtree is balanced, so we return its height: 1 + max(l, r)
Final Result:
The main function simply calls height(root)
and checks if the result is non-negative:
- If
height(root) >= 0
: The tree is balanced, returnTrue
- If
height(root) == -1
: The tree is unbalanced, returnFalse
The algorithm visits each node exactly once, giving us O(n)
time complexity, where n
is the number of nodes. The space complexity is O(h)
where h
is the height of the tree, due to the recursive call stack.
The elegance of this solution lies in using -1
as a dual-purpose signal: it both indicates an unbalanced tree and allows early termination without needing additional boolean flags or separate functions.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution works.
Consider this binary tree:
1 / \ 2 3 / \ 4 5 / 6
We'll trace through the recursive calls to see how the algorithm determines this tree is unbalanced.
Step-by-step execution:
- Start with
height(1)
- we need to check node 1 (root)- First, calculate
height(2)
for the left subtree
- First, calculate
- At node 2:
- Calculate
height(4)
for its left child
- Calculate
- At node 4:
- Calculate
height(6)
for its left child
- Calculate
- At node 6:
height(None)
for left child returns 0height(None)
for right child returns 0- Balance check: |0 - 0| = 0 ≤ 1 ✓
- Return: 1 + max(0, 0) = 1
- Back at node 4:
- Left height (from node 6) = 1
height(None)
for right child returns 0- Balance check: |1 - 0| = 1 ≤ 1 ✓
- Return: 1 + max(1, 0) = 2
- Back at node 2:
- Left height (from node 4) = 2
- Calculate
height(5)
for right child
- At node 5:
height(None)
for both children returns 0- Balance check: |0 - 0| = 0 ≤ 1 ✓
- Return: 1 + max(0, 0) = 1
- Back at node 2:
- Left height = 2, Right height = 1
- Balance check: |2 - 1| = 1 ≤ 1 ✓
- Return: 1 + max(2, 1) = 3
- Back at node 1:
- Left height (from node 2) = 3
- Calculate
height(3)
for right child
- At node 3:
height(None)
for both children returns 0- Balance check: |0 - 0| = 0 ≤ 1 ✓
- Return: 1 + max(0, 0) = 1
- Final check at node 1:
- Left height = 3, Right height = 1
- Balance check: |3 - 1| = 2 > 1 ✗
- Return: -1 (unbalanced!)
- Main function receives -1, so returns
False
The tree is unbalanced because at the root node, the left subtree has height 3 while the right subtree has height 1, giving a difference of 2, which violates the balance condition.
Note how the algorithm efficiently combines height calculation with balance checking in a single traversal, and immediately returns -1 when it detects the imbalance at the root, without needing to check any remaining nodes.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional
9
10class Solution:
11 def isBalanced(self, root: Optional[TreeNode]) -> bool:
12 """
13 Determines if a binary tree is height-balanced.
14 A height-balanced tree is defined as a tree where the depth of the two subtrees
15 of every node never differs by more than 1.
16
17 Args:
18 root: The root node of the binary tree
19
20 Returns:
21 True if the tree is balanced, False otherwise
22 """
23
24 def calculate_height(node: Optional[TreeNode]) -> int:
25 """
26 Calculates the height of a subtree while checking if it's balanced.
27
28 Args:
29 node: Current node being processed
30
31 Returns:
32 The height of the subtree if balanced, -1 if unbalanced
33 """
34 # Base case: empty node has height 0
35 if node is None:
36 return 0
37
38 # Recursively calculate heights of left and right subtrees
39 left_height = calculate_height(node.left)
40 right_height = calculate_height(node.right)
41
42 # Check if any subtree is unbalanced or if current node violates balance condition
43 if (left_height == -1 or
44 right_height == -1 or
45 abs(left_height - right_height) > 1):
46 return -1 # Return -1 to indicate unbalanced tree
47
48 # Return height of current subtree (1 + maximum height of children)
49 return 1 + max(left_height, right_height)
50
51 # Tree is balanced if height calculation doesn't return -1
52 return calculate_height(root) >= 0
53
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Determines if a binary tree is height-balanced.
19 * A binary tree is balanced if the depth difference between left and right subtrees
20 * of every node is at most 1.
21 *
22 * @param root The root node of the binary tree
23 * @return true if the tree is balanced, false otherwise
24 */
25 public boolean isBalanced(TreeNode root) {
26 // If height calculation returns -1, tree is unbalanced
27 // Otherwise (height >= 0), tree is balanced
28 return height(root) >= 0;
29 }
30
31 /**
32 * Calculates the height of a subtree while checking for balance.
33 * Returns -1 if the subtree is unbalanced.
34 *
35 * @param root The root node of the subtree
36 * @return The height of the subtree if balanced, -1 if unbalanced
37 */
38 private int height(TreeNode root) {
39 // Base case: empty tree has height 0
40 if (root == null) {
41 return 0;
42 }
43
44 // Recursively calculate height of left subtree
45 int leftHeight = height(root.left);
46
47 // Recursively calculate height of right subtree
48 int rightHeight = height(root.right);
49
50 // Check if any subtree is unbalanced or height difference exceeds 1
51 if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
52 // Propagate unbalanced state up the tree
53 return -1;
54 }
55
56 // Return height of current subtree (1 + maximum child height)
57 return 1 + Math.max(leftHeight, rightHeight);
58 }
59}
60
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Determines if a binary tree is height-balanced.
16 * A height-balanced binary tree is defined as a tree in which the depth
17 * of the two subtrees of every node never differs by more than 1.
18 *
19 * @param root The root node of the binary tree
20 * @return true if the tree is balanced, false otherwise
21 */
22 bool isBalanced(TreeNode* root) {
23 // Lambda function to calculate height and check balance simultaneously
24 // Returns -1 if the tree is unbalanced, otherwise returns the height
25 function<int(TreeNode*)> calculateHeight = [&](TreeNode* node) -> int {
26 // Base case: empty node has height 0
27 if (!node) {
28 return 0;
29 }
30
31 // Recursively calculate the height of left subtree
32 int leftHeight = calculateHeight(node->left);
33
34 // Recursively calculate the height of right subtree
35 int rightHeight = calculateHeight(node->right);
36
37 // Check if any subtree is unbalanced or if current node violates balance condition
38 // If left or right subtree is unbalanced (returns -1), or
39 // if the height difference exceeds 1, mark as unbalanced
40 if (leftHeight == -1 || rightHeight == -1 || abs(leftHeight - rightHeight) > 1) {
41 return -1; // Indicates unbalanced tree
42 }
43
44 // Return the height of current node (1 + maximum height of subtrees)
45 return 1 + max(leftHeight, rightHeight);
46 };
47
48 // Tree is balanced if the height calculation doesn't return -1
49 return calculateHeight(root) >= 0;
50 }
51};
52
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Determines if a binary tree is height-balanced.
17 * A height-balanced tree is defined as a tree where the depth of the two subtrees
18 * of every node never differs by more than 1.
19 *
20 * @param root - The root node of the binary tree
21 * @returns true if the tree is balanced, false otherwise
22 */
23function isBalanced(root: TreeNode | null): boolean {
24 /**
25 * Helper function that calculates the height of a subtree.
26 * Returns -1 if the subtree is unbalanced, otherwise returns the actual height.
27 *
28 * @param node - The current node being processed
29 * @returns The height of the subtree if balanced, -1 if unbalanced
30 */
31 const calculateHeight = (node: TreeNode | null): number => {
32 // Base case: empty node has height 0
33 if (node === null) {
34 return 0;
35 }
36
37 // Recursively calculate the height of left subtree
38 const leftHeight: number = calculateHeight(node.left);
39
40 // Recursively calculate the height of right subtree
41 const rightHeight: number = calculateHeight(node.right);
42
43 // Check if any subtree is unbalanced or if current node violates balance condition
44 if (leftHeight === -1 || rightHeight === -1 || Math.abs(leftHeight - rightHeight) > 1) {
45 return -1; // Propagate unbalanced state up the tree
46 }
47
48 // Return the height of current subtree (1 + maximum height of children)
49 return 1 + Math.max(leftHeight, rightHeight);
50 };
51
52 // Tree is balanced if the helper function doesn't return -1
53 return calculateHeight(root) !== -1;
54}
55
Time and Space Complexity
Time Complexity: O(n)
The algorithm visits each node in the binary tree exactly once through a post-order traversal (visiting left subtree, right subtree, then the current node). At each node, it performs constant time operations: calculating the height difference and returning either -1
or 1 + max(l, r)
. Since we visit all n
nodes once and perform O(1)
work at each node, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity is determined by the recursive call stack. In the worst case, the tree could be completely unbalanced (e.g., a skewed tree where each node has only one child), resulting in a recursion depth equal to the number of nodes n
. This would require O(n)
space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n)
, but we consider the worst-case scenario for space complexity analysis, giving us O(n)
.
Common Pitfalls
1. Attempting Top-Down Approach with Separate Height Calculation
A common mistake is trying to solve this problem by checking balance at each node from top to bottom, calculating heights separately each time:
# Inefficient approach - DON'T DO THIS
def isBalanced(self, root):
if not root:
return True
def getHeight(node):
if not node:
return 0
return 1 + max(getHeight(node.left), getHeight(node.right))
# Calculate height for each subtree separately
left_height = getHeight(root.left)
right_height = getHeight(root.right)
# Check current node and recursively check children
return (abs(left_height - right_height) <= 1 and
self.isBalanced(root.left) and
self.isBalanced(root.right))
Why this is problematic:
- Time complexity becomes O(n²) because we calculate heights repeatedly for the same nodes
- For each node, we traverse its entire subtree to get the height
- Lower nodes get visited multiple times unnecessarily
2. Forgetting to Check Subtree Balance Status
Another mistake is only checking the height difference without verifying if subtrees themselves are balanced:
# Incorrect - only checks current node's balance
def calculate_height(node):
if node is None:
return 0
left_height = calculate_height(node.left)
right_height = calculate_height(node.right)
# WRONG: Only checking current node, ignoring subtree balance
if abs(left_height - right_height) > 1:
return -1
return 1 + max(left_height, right_height)
The fix: Always check if either subtree returned -1 before checking the current node's balance:
# Correct version
if left_height == -1 or right_height == -1:
return -1 # Propagate the unbalanced signal
if abs(left_height - right_height) > 1:
return -1
3. Confusing Height with Depth or Number of Nodes
Some implementations mistakenly count nodes instead of edges:
# Incorrect - counting nodes instead of edges
def calculate_height(node):
if node is None:
return 0
if node.left is None and node.right is None:
return 1 # WRONG: leaf should have height 0, not 1
Remember:
- Height = number of edges on the longest path to a leaf
- A single node has height 0
- An empty tree has height -1 (though we use 0 for convenience in this problem)
4. Not Handling the -1 Signal Properly
When using -1 as an "unbalanced" indicator, ensure it's handled correctly:
# Incorrect final check
def isBalanced(self, root):
return self.calculate_height(root) != -1 # Should be >= 0 for clarity
# Or worse - forgetting the special meaning of -1
def isBalanced(self, root):
return self.calculate_height(root) > 0 # WRONG: height 0 is valid!
Best practice: Use >= 0
to make the intention clear that any non-negative height means the tree is balanced.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!