Solving Maximum Depth of Binary Tree in JavaScript: Step-by-Step Guide
The Maximum Depth of Binary Tree problem is one of the most foundational challenges you will encounter when learning tree data structures. It asks a deceptively simple question: What is the longest path from the root node down to the farthest leaf node? Despite its simplicity, mastering this problem unlocks a deeper understanding of recursion, traversal strategies, and algorithmic thinking that applies to countless other tree-based challenges.
What Is the Maximum Depth of a Binary Tree?
A binary tree is a hierarchical data structure where each node has at most two children, typically referred to as the left and right child. The maximum depth (also called the height) of a binary tree is the number of nodes along the longest path from the root node down to the farthest leaf node. By convention, an empty tree has a depth of 0, and a tree with only a root node has a depth of 1.
Consider the following binary tree:
3
/ \
9 20
/ \
15 7
The longest path is 3 -> 20 -> 15 or 3 -> 20 -> 7, both of which contain 3 nodes. Therefore, the maximum depth is 3.
Why This Problem Matters
Understanding how to compute the maximum depth is essential for several reasons:
- Foundational recursion practice: It teaches you how to break a problem into smaller subproblems and combine their results.
- Tree traversal mastery: You learn to navigate trees using depth-first and breadth-first strategies.
- Interview relevance: This is a classic LeetCode problem (LeetCode #104) frequently asked in coding interviews at major tech companies.
- Building block for advanced problems: Concepts here extend to balanced tree checks, diameter of a tree, and path sum problems.
- Real-world applications: Tree depth calculations are used in DOM manipulation, file system traversal, and parsing hierarchical data like JSON or XML.
Defining the Binary Tree Node
Before solving the problem, we need a way to represent a binary tree in JavaScript. The standard approach is to define a TreeNode class:
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
Each node stores a value and references to its left and right children. We can construct the example tree above like this:
const root = new TreeNode(
3,
new TreeNode(9),
new TreeNode(20, new TreeNode(15), new TreeNode(7))
);
Approach 1: Recursive Depth-First Search (DFS)
The most intuitive solution uses recursion. The key insight is that the maximum depth of a tree is 1 + max(depth(leftSubtree), depth(rightSubtree)). The base case occurs when the node is null, meaning we have reached beyond a leaf, so we return 0.
function maxDepth(root) {
// Base case: an empty tree has depth 0
if (root === null) {
return 0;
}
// Recursively compute the depth of left and right subtrees
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
// The depth of the current node is 1 plus the larger subtree depth
return 1 + Math.max(leftDepth, rightDepth);
}
Let us trace through the example tree:
- At node
3, we recurse into left child9and right child20. - At node
9, both children arenull, so it returns1 + max(0, 0) = 1. - At node
20, we recurse into15and7, each returning1. - Node
20returns1 + max(1, 1) = 2. - Node
3returns1 + max(1, 2) = 3.
This approach has a time complexity of O(n), where n is the number of nodes, because we visit each node exactly once. The space complexity is O(h), where h is the height of the tree, due to the recursion stack. In the worst case (a skewed tree), this becomes O(n).
Approach 2: Iterative Depth-First Search Using a Stack
If you want to avoid recursion (perhaps to prevent stack overflow on very deep trees), you can simulate the same logic iteratively using an explicit stack. Each stack entry stores the current node and its accumulated depth.
function maxDepthIterativeDFS(root) {
if (root === null) {
return 0;
}
const stack = [[root, 1]];
let maxDepthFound = 0;
while (stack.length > 0) {
const [node, depth] = stack.pop();
if (node !== null) {
maxDepthFound = Math.max(maxDepthFound, depth);
stack.push([node.left, depth + 1]);
stack.push([node.right, depth + 1]);
}
}
return maxDepthFound;
}
Here, we push child nodes onto the stack along with their incremented depth. Whenever we pop a node, we update the maximum depth found so far. This preserves the O(n) time complexity and O(h) space complexity of the recursive version.
Approach 3: Iterative Breadth-First Search (BFS)
An alternative strategy processes the tree level by level. Each complete level we traverse increments the depth counter by one. When there are no more levels to process, the counter holds the maximum depth. This approach uses a queue.
function maxDepthBFS(root) {
if (root === null) {
return 0;
}
const queue = [root];
let depth = 0;
while (queue.length > 0) {
const levelSize = queue.length;
depth++;
// Process all nodes at the current level
for (let i = 0; i < levelSize; i++) {
const currentNode = queue.shift();
if (currentNode.left !== null) {
queue.push(currentNode.left);
}
if (currentNode.right !== null) {
queue.push(currentNode.right);
}
}
}
return depth;
}
The BFS approach is especially intuitive because depth directly corresponds to the number of levels processed. Its time complexity is also O(n), and its space complexity is O(w), where w is the maximum width of the tree. For a balanced tree, this can be more memory-efficient than DFS in certain scenarios, though for a skewed tree both degrade to O(n).
Comparing the Three Approaches
- Recursive DFS: Cleanest and most readable. Best for interviews when you want concise, elegant code. Risk of stack overflow on extremely deep trees.
- Iterative DFS: Avoids recursion limits while keeping the same logic. Slightly more verbose due to manual stack management.
- Iterative BFS: Naturally maps depth to levels. Useful when you also need level-order information. May use more memory on wide trees.
Best Practices
When implementing this solution in real projects or interviews, keep the following best practices in mind:
- Always handle the empty tree case: Returning
0for anullroot prevents runtime errors and matches the standard definition. - Prefer recursion for clarity: Unless you have a specific reason to avoid recursion (such as extremely deep trees), the recursive solution is the most maintainable.
- Use descriptive variable names: Names like
leftDepthandrightDepthmake the logic self-documenting. - Test edge cases: Always test with an empty tree, a single-node tree, a left-skewed tree, a right-skewed tree, and a balanced tree.
- Understand the trade-offs: Know when DFS versus BFS is more appropriate based on tree shape and memory constraints.
- Avoid mutating input: Your function should not modify the original tree structure.
Testing the Solution
Here is a small test suite covering common scenarios:
function runTests() {
// Test 1: Empty tree
console.log(maxDepth(null) === 0 ? "PASS" : "FAIL");
// Test 2: Single node
console.log(maxDepth(new TreeNode(1)) === 1 ? "PASS" : "FAIL");
// Test 3: Balanced tree
const balanced = new TreeNode(
3,
new TreeNode(9),
new TreeNode(20, new TreeNode(15), new TreeNode(7))
);
console.log(maxDepth(balanced) === 3 ? "PASS" : "FAIL");
// Test 4: Left-skewed tree
const leftSkewed = new TreeNode(1, new TreeNode(2, new TreeNode(3)));
console.log(maxDepth(leftSkewed) === 3 ? "PASS" : "FAIL");
// Test 5: Right-skewed tree
const rightSkewed = new TreeNode(1, null, new TreeNode(2, null, new TreeNode(3)));
console.log(maxDepth(rightSkewed) === 3 ? "PASS" : "FAIL");
}
runTests();
Running these tests confirms that the recursive solution handles all major tree shapes correctly. You can swap in the iterative implementations to verify they produce identical results.
Common Pitfalls to Avoid
- Forgetting the base case: Omitting the
nullcheck leads to aTypeErrorwhen accessingroot.lefton an empty node. - Returning depth instead of
1 + max(...): A common mistake is forgetting to add 1 for the current node, which undercounts the depth. - Confusing depth with the number of edges: Some definitions count edges rather than nodes. Clarify which convention your interviewer or problem statement uses.
- Infinite recursion: Ensure your recursive calls always move toward the base case by passing child nodes, not the same node repeatedly.
Conclusion
The Maximum Depth of Binary Tree problem is a perfect entry point into the world of tree algorithms. By exploring recursive DFS, iterative DFS, and iterative BFS, you gain a versatile toolkit that transfers directly to more complex challenges like computing tree diameter, checking balance, and finding lowest common ancestors. The recursive solution remains the gold standard for its elegance and readability, while the iterative variants demonstrate how to reason about traversal without relying on the call stack. Master this problem, and you will have built a solid foundation for tackling virtually any tree-based question that comes your way in both interviews and real-world development.