L15. Check for Balanced Binary Tree
A comprehensive study note on validating whether a binary tree is height-balanced, transitioning from an inefficient brute-force approach to an optimized single-pass solution using clever recursive state encoding.
1. Problem Statement & Definition of a Balanced Binary Tree
In data structures and algorithms, determining tree balance is a fundamental validation problem. A Balanced Binary Tree (often referred to in libraries like AVL trees) is defined as a binary tree in which the height difference between the left subtree and the right subtree of every single node is less than or equal to 1.
Formally, for every node in the tree, the following condition must hold true:
If even a single node violates this rule, the entire binary tree is classified as unbalanced.
2. Core Mental Model & Visualizing Balance
Real-Life Analogy
Imagine balancing a mobile hanging from the ceiling or an organizational hierarchy where every manager's left and right departments must remain roughly equal in depth. If one side grows excessively deep while the other remains shallow, the structure tilts dangerously, losing its equilibrium.
Visualizing Balanced vs. Unbalanced Trees
Example 1: Balanced Binary Tree
3
/ \
9 20
/ \
15 7
- At Node
20: Left height =1(node15), Right height =1(node7). Difference =|1 - 1| = 0(Balanced). - At Node
3: Left height =1(node9), Right height =2(sub-branch20->15). Difference =|1 - 2| = 1(Balanced). - Result: The tree is balanced.
Example 2: Unbalanced Binary Tree
1
/
2
/
3
/
4
\
5
- At Node
1: Left subtree depth is3(nodes2->3->4), while the right subtree depth is0. Difference =|3 - 0| = 3, which is . - Result: The tree is not balanced.
3. The Naive Approach () and Why It Fails
The Intuitive Strategy
The most straightforward way to solve this problem is to visit every node individually. For each node encountered during traversal:
- Calculate the height of its left subtree.
- Calculate the height of its right subtree.
- Check if the absolute difference exceeds
1. If it does, immediately returnfalse. - Recursively repeat this check for the left and right children.
Pseudo-Code for Naive Approach
function isBalancedNaive(node: TreeNode | null): boolean {
if (node === null) return true;
const leftHeight = getHeight(node.left);
const rightHeight = getHeight(node.right);
if (Math.abs(leftHeight - rightHeight) > 1) {
return false;
}
return isBalancedNaive(node.left) && isBalancedNaive(node.right);
}
function getHeight(node: TreeNode | null): number {
if (node === null) return 0;
return 1 + Math.max(getHeight(node.left), getHeight(node.right));
}
Complexity Analysis of Naive Approach
- Time Complexity: in the worst-case scenario (such as a skewed tree), because for each of the nodes, we traverse down its subtree to calculate heights, leading to nested linear operations.
- Space Complexity: auxiliary stack space for recursion.
- Verdict: Unacceptable for large datasets in production environments. We must eliminate the redundant height calculations.
4. The Optimized Approach: Merging Height Calculation with Balance Checking ()
The Breakthrough Idea
To optimize the time complexity down to , we need to compute the height of the tree and check for balance simultaneously in a single bottom-up post-order traversal.
Normally, a standard height-finding function returns an integer representing height. A boolean function returns true or false. How do we combine them?
State Encoding Trick:
- If a subtree is balanced, the function returns its actual height (an integer ).
- If a subtree is unbalanced, the function returns a sentinel flag value:
-1.
By bubbling -1 up the call stack, any parent node instantly knows that a violation occurred deep below, allowing it to short-circuit and terminate early without performing unnecessary checks.
5. Step-by-Step Code Evolution
Optimized Recursive Function (checkHeight)
int checkHeight(TreeNode* node) {
if (node == nullptr) return 0;
// 1. Get left subtree height
int leftHeight = checkHeight(node->left);
if (leftHeight == -1) return -1; // Short-circuit if left is unbalanced
// 2. Get right subtree height
int rightHeight = checkHeight(node->right);
if (rightHeight == -1) return -1; // Short-circuit if right is unbalanced
// 3. Check current node balance condition
if (abs(leftHeight - rightHeight) > 1) {
return -1; // Mark current subtree as unbalanced
}
// 4. Return actual height if balanced
return 1 + max(leftHeight, rightHeight);
}
6. Complete Implementation (C++ and Java)
Below are the complete, production-ready implementations in both C++ and Java.
C++ Implementation
#include <algorithm>
#include <cmath>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
private:
int dfsHeight(TreeNode* node) {
// Base case: empty tree has height 0
if (node == nullptr) return 0;
// Recursively check left subtree
int leftHeight = dfsHeight(node->left);
if (leftHeight == -1) return -1;
// Recursively check right subtree
int rightHeight = dfsHeight(node->right);
if (rightHeight == -1) return -1;
// Check balance condition for current node
if (abs(leftHeight - rightHeight) > 1) {
return -1;
}
// Return height of current node
return 1 + max(leftHeight, rightHeight);
}
public:
bool isBalanced(TreeNode* root) {
return dfsHeight(root) != -1;
}
};
Java Implementation
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
class Solution {
public boolean isBalanced(TreeNode root) {
return dfsHeight(root) != -1;
}
private int dfsHeight(TreeNode node) {
// Base case: empty tree has height 0
if (node == null) return 0;
// Recursively check left subtree
int leftHeight = dfsHeight(node.left);
if (leftHeight == -1) return -1;
// Recursively check right subtree
int rightHeight = dfsHeight(node.right);
if (rightHeight == -1) return -1;
// Check balance condition for current node
if (Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
// Return height of current node
return 1 + Math.max(leftHeight, rightHeight);
}
}
7. Dry Run Walkthrough
Consider checking the balance of this tree:
1
/ \
2 0
/
3
- Call
isBalanced(1)-> invokesdfsHeight(1). dfsHeight(1)callsdfsHeight(2).dfsHeight(2)callsdfsHeight(3).dfsHeight(3)hasleft = null(returns0) andright = null(returns0).- Difference:
|0 - 0| = 0. - Returns height:
1 + max(0, 0) = 1to node2.
- Difference:
- Back at
dfsHeight(2):leftHeight = 1. Now calls right child:dfsHeight(null)which returns0.- Difference:
|1 - 0| = 1. - Returns height:
1 + max(1, 0) = 2to root node1.
- Difference:
- Back at root
dfsHeight(1):leftHeight = 2. Now calls right child:dfsHeight(0).- Node
0is a leaf node, returns height1. - Difference at root
1:|leftHeight - rightHeight| = |2 - 1| = 1. - Returns height:
1 + max(2, 1) = 3.
- Node
- Final Result:
dfsHeight(root)returns3(not-1), soisBalancedreturnstrue.
8. Complexity Summary Table
9. Summary and Key Takeaways
- Definition of Balance: A binary tree is balanced if for every node, the height difference between its left and right subtrees is at most
1. - Avoid Redundant Work: Naive solutions recalculate subtree heights repeatedly, resulting in an inefficient time complexity.
- State Encoding Pattern: By letting height-checking functions return
-1upon detecting an imbalance, we combine validation and height computation into a single pass. - Optimal Efficiency: The optimized recursive algorithm visits each node exactly once, achieving an optimal time complexity.
- Short-Circuiting: As soon as a
-1sentinel value is encountered, recursion halts further traversal up the call stack, saving CPU cycles. - Space Complexity Reality: While time complexity drops to , stack memory remains in the worst case (e.g., skewed trees).