Balanced Binary Tree
EasyPrompt
Given a binary tree, determine if it is height-balanced.
Example 1:
Input: root = [3,9,20,null,null,15,7]
Output: trueExample 2:
Input: root = [1,2,2,3,3,null,null,4,4]
Output: falseExample 3:
Input: root = []
Output: true
Constraints:
- The number of nodes in the tree is in the range
[0, 5000]. -104 <= Node.val <= 104
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly translates the definition of a balanced binary tree into a recursive algorithm. For each node, it first calculates the heights of its left and right subtrees and checks if the difference is at most one. If this condition holds, it then recursively proceeds to check if the left and right subtrees are themselves balanced. This method is straightforward to understand but is inefficient because it repeatedly calculates the height of the same subtrees.
Algorithm
- Define a helper function
height(node)that recursively computes the height of a subtree. The height of a null node is 0, and the height of a non-null node is1 + max(height(node.left), height(node.right)). - Define the main function
isBalanced(root). - If the
rootis null, the tree is balanced, so returntrue. - Calculate the height of the left subtree,
leftHeight = height(root.left). - Calculate the height of the right subtree,
rightHeight = height(root.right). - Check if the current node is balanced:
if (Math.abs(leftHeight - rightHeight) > 1), returnfalse. - If the current node is balanced, recursively check if its subtrees are also balanced:
return isBalanced(root.left) && isBalanced(root.right).
Walkthrough
The core idea is to separate the problem into two distinct recursive functions: one to check for balance (isBalanced) and another to calculate height (height).
-
height(TreeNode node)function: This helper function computes the height of a subtree rooted atnode. It's a standard recursive height calculation. If the node is null, its height is 0. Otherwise, its height is 1 plus the maximum height of its left and right children. -
isBalanced(TreeNode root)function: This is the main function. For any given node, it performs three checks: a. It computes the heights of the left and right subtrees using theheighthelper. b. It checks if the absolute difference between these heights is greater than 1. If it is, the tree is not balanced, and it returnsfalse. c. If the current node is balanced, it must also ensure that all its descendants are balanced. It does this by recursively callingisBalancedon its left and right children.
The entire tree is considered balanced only if the balance property holds true for every single node in the tree. The major performance issue arises from the fact that isBalanced calls height, and both functions traverse the subtrees. This means nodes are visited multiple times, leading to overlapping subproblems that are re-computed.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { // Helper function to calculate the height of a tree. private int height(TreeNode node) { if (node == null) { return 0; // Height of an empty tree is 0. } // Height is 1 + max height of left/right subtrees. return 1 + Math.max(height(node.left), height(node.right)); } public boolean isBalanced(TreeNode root) { // An empty tree is balanced. if (root == null) { return true; } // Get heights of left and right subtrees. int leftHeight = height(root.left); int rightHeight = height(root.right); // Check if the current node is balanced AND // if the left and right subtrees are also balanced. if (Math.abs(leftHeight - rightHeight) <= 1 && isBalanced(root.left) && isBalanced(root.right)) { return true; } return false; }}Complexity
Time
O(N^2) in the worst case (a skewed tree). For each of the N nodes, we might traverse its entire subtree to calculate the height. For a balanced tree, the complexity is closer to O(N log N).
Space
O(N) in the worst case (a skewed tree) and O(log N) in the best case (a balanced tree), due to the recursion stack depth.
Trade-offs
Pros
Intuitive and easy to understand as it directly models the problem statement.
Clearly separates the logic for height calculation and balance checking.
Cons
Highly inefficient due to redundant computations. The height of a single node might be calculated multiple times as the recursion progresses down the tree.
The time complexity is poor, especially for skewed trees, which can lead to a 'Time Limit Exceeded' error on platforms like LeetCode.
This optimized approach combines the height calculation and balance check into a single recursive pass. It works in a bottom-up manner (similar to a post-order traversal). A helper function is designed to return the height of a subtree only if it's balanced. If an imbalance is detected at any node, it returns a special sentinel value (like -1). This sentinel value is then propagated up the recursion stack, allowing for an early exit and avoiding unnecessary computations. This reduces the time complexity from quadratic to linear.
Algorithm
- Define a helper function,
checkHeight(node), that returns the height of the subtree if it's balanced, or a sentinel value (e.g., -1) if it's not. - Base Case: If
nodeis null, it's a balanced subtree of height 0. Return 0. - Recursive Step:
- Recursively call
leftHeight = checkHeight(node.left). IfleftHeightis -1, an imbalance was found below. Propagate the error by immediately returning -1. - Recursively call
rightHeight = checkHeight(node.right). IfrightHeightis -1, return -1. - Check the balance condition at the current node:
if (Math.abs(leftHeight - rightHeight) > 1), the current subtree is unbalanced, so return -1. - If all checks pass, the current subtree is balanced. Return its height:
1 + Math.max(leftHeight, rightHeight).
- Recursively call
- The main
isBalanced(root)function callscheckHeight(root)and returnstrueif the result is not -1, andfalseotherwise.
Walkthrough
This approach improves upon the brute-force method by eliminating redundant calculations. Instead of separating height calculation and balance checking, we merge them into a single recursive function that performs a post-order traversal of the tree.
The helper function, let's call it checkHeight, is designed to return two kinds of information:
- The height of the subtree: If the subtree rooted at the current node is balanced.
- An error code (-1): If the subtree is unbalanced.
This function works as follows:
- It recursively calls itself for the left and right children.
- When the recursive calls return, it first checks if either of them returned -1. If so, it means an imbalance has already been detected in a lower part of the tree. In this case, there's no need to check the current node; we simply propagate the -1 up the call stack.
- If both subtrees are balanced (i.e., their heights are non-negative), we then check the balance condition at the current node:
Math.abs(leftHeight - rightHeight) > 1. If this condition is met, the current node is the source of an imbalance, so we return -1. - If the current node is also balanced, we calculate its height as
1 + Math.max(leftHeight, rightHeight)and return it to the parent caller.
The main isBalanced function simply initiates the process by calling checkHeight(root) and checking if the final result is -1. This ensures that every node is visited only once, making the algorithm very efficient.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { // Helper function returns the height of a subtree if it's balanced, // otherwise returns -1. private int checkHeight(TreeNode node) { // Base case: An empty tree is balanced and has a height of 0. if (node == null) { return 0; } // Recursively check the left subtree. int leftHeight = checkHeight(node.left); // If the left subtree is unbalanced, propagate the imbalance signal up. if (leftHeight == -1) { return -1; } // Recursively check the right subtree. int rightHeight = checkHeight(node.right); // If the right subtree is unbalanced, propagate the imbalance signal up. if (rightHeight == -1) { return -1; } // Check if the current node is balanced. if (Math.abs(leftHeight - rightHeight) > 1) { return -1; // Signal imbalance. } // If balanced, return the height of the current node's subtree. return 1 + Math.max(leftHeight, rightHeight); } public boolean isBalanced(TreeNode root) { // A tree is balanced if its checkHeight is not -1. return checkHeight(root) != -1; }}Complexity
Time
O(N), where N is the number of nodes in the tree, because we visit each node exactly once.
Space
O(H), where H is the height of the tree. This space is used by the recursion stack. In the worst case of a skewed tree, H = N, so the complexity is O(N). For a balanced tree, it's O(log N).
Trade-offs
Pros
Optimal time complexity of O(N) as each node is visited only once.
Efficiently combines height calculation and balance checking into a single pass.
Allows for early termination. As soon as an imbalance is found, it stops processing that branch and returns.
Cons
The logic can be slightly less direct to grasp compared to the brute-force approach because the return value is overloaded (it represents either a valid height or an error code).
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public boolean isBalanced ( TreeNode root ) { return height ( root ) >= 0 ; } private int height ( TreeNode root ) { if ( root == null ) { return 0 ; } int l = height ( root . left ); int r = height ( root . right ); if ( l == - 1 || r == - 1 || Math . abs ( l - r ) > 1 ) { return - 1 ; } return 1 + Math . max ( l , r ); } }/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @return {boolean} */ var isBalanced = function (root) { const height = (root) => { if (!root) { return 0; } const l = height(root.left); const r = height(root.right); if (l == -1 || r == -1 || Math.abs(l - r) > 1) { return -1; } return 1 + Math.max(l, r); }; return height(root) >= 0; };/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: bool isBalanced ( TreeNode * root ) { function < int ( TreeNode * ) > height = [ & ]( TreeNode * root ) { if ( ! root ) { return 0 ; } int l = height ( root -> left ); int r = height ( root -> right ); if ( l == - 1 || r == - 1 || abs ( l - r ) > 1 ) { return - 1 ; } return 1 + max ( l , r ); }; return height ( root ) >= 0 ; } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def isBalanced ( self , root : Optional [ TreeNode ]) -> bool : def height ( root ): if root is None : return 0 l , r = height ( root . left ), height ( root . right ) if l == - 1 or r == - 1 or abs ( l - r ) > 1 : return - 1 return 1 + max ( l , r ) return height ( root ) >= 0Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.