Facebook Pixel

559. Maximum Depth of N-ary Tree

Problem Description

You are given an n-ary tree (a tree where each node can have any number of children, not just two like in a binary tree). Your task is to find the maximum depth of this tree.

The maximum depth is defined as the total number of nodes along the longest path from the root node down to the farthest leaf node. In other words, you need to find the longest chain of nodes from root to any leaf.

For example, if you have a tree where the root has three children, and one of those children has two more children of its own, the paths would be:

  • Root → Child1 (depth = 2)
  • Root → Child2 (depth = 2)
  • Root → Child3 → Grandchild1 (depth = 3)
  • Root → Child3 → Grandchild2 (depth = 3)

The maximum depth would be 3 in this case.

The tree is represented using a Node class where each node has:

  • A value (val)
  • A list of children nodes (children)

If the tree is empty (root is None), the depth should be 0.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: An n-ary tree is a special type of graph where nodes are connected by edges, with each node having multiple children and no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with an n-ary tree structure, where we have a root node and each node can have multiple children, forming a hierarchical structure without cycles.

DFS

  • Conclusion reached: Since we're dealing with a tree structure, the flowchart leads us directly to DFS (Depth-First Search).

The DFS pattern is perfect for this problem because:

  1. We need to explore each path from root to leaf completely to determine its depth
  2. We need to compare depths of all possible paths to find the maximum
  3. DFS naturally traverses from root to leaves, calculating depth as it goes
  4. The recursive nature of DFS matches the recursive structure of trees

Conclusion: The flowchart suggests using a DFS approach for finding the maximum depth of an n-ary tree. This aligns perfectly with the solution, which recursively explores each child subtree, finds their maximum depths, and returns the maximum among all children plus 1 (for the current node).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we think about finding the maximum depth of a tree, we need to understand what depth really means - it's the number of nodes from root to the farthest leaf. This naturally suggests a recursive approach because a tree itself is a recursive structure.

The key insight is that the maximum depth of any tree can be expressed in terms of its subtrees. If we're standing at the root node, the maximum depth of the entire tree is simply 1 (for the root itself) plus the maximum depth among all its subtrees.

Think of it like this: imagine you're trying to find the tallest building in a city with multiple districts. You could ask each district, "What's your tallest building?" Then, the tallest building in the entire city would be the maximum among all district answers. Similarly, for a tree node, we ask each child subtree, "What's your maximum depth?" and take the largest value.

The recursive pattern emerges naturally:

  • Base case: If we reach a null node (no tree exists), the depth is 0
  • Recursive case: For any node, its depth contribution is 1 plus the maximum depth of its children

For an n-ary tree where each node can have multiple children, we need to check all children and find which subtree goes deepest. The formula becomes: depth(node) = 1 + max(depth(child1), depth(child2), ..., depth(childN)).

This DFS approach ensures we explore each path completely before comparing, which is exactly what we need to find the longest path from root to leaf.

Learn more about Tree, Depth-First Search and Breadth-First Search patterns.

Solution Approach

The solution implements a recursive DFS approach as identified through our flowchart analysis. Let's walk through the implementation step by step:

Base Case Check: The function first checks if root is None. If the tree is empty, there are no nodes to count, so we return 0. This serves as our recursion termination condition.

Initialize Maximum Depth Tracker: We initialize a variable mx to 0. This variable will keep track of the maximum depth among all child subtrees of the current node.

Traverse All Children: The algorithm iterates through all children of the current node using for child in root.children. For each child:

  • We make a recursive call self.maxDepth(child) to find the maximum depth of that child's subtree
  • We update mx using max(mx, self.maxDepth(child)) to keep only the largest depth value found so far

Calculate Current Node's Depth: After checking all children and finding the maximum depth among them, we return 1 + mx. The 1 accounts for the current node itself, and mx represents the maximum depth of its subtrees.

How the Recursion Works:

  1. Starting from the root, the function explores each branch completely before moving to the next
  2. When it reaches a leaf node (a node with no children), the loop doesn't execute, mx remains 0, and it returns 1
  3. As the recursion unwinds, each parent node adds 1 to the maximum depth reported by its children
  4. The process continues until we reach back to the root, which returns the final maximum depth

This approach has a time complexity of O(n) where n is the total number of nodes, as we visit each node exactly once. The space complexity is O(h) where h is the height of the tree, due to the recursion call stack.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to see how the recursive DFS solution works.

Consider this n-ary tree:

        1
       /|\
      3 2 4
     / \
    5   6

We want to find the maximum depth, which should be 3 (the path 1→3→5 or 1→3→6).

Step-by-step execution:

  1. Call maxDepth(node 1):
    • Node 1 is not None, so we continue
    • Initialize mx = 0
    • Node 1 has 3 children: [3, 2, 4]
    • For child 3: Call maxDepth(node 3)
  2. Call maxDepth(node 3) (first child of 1):
    • Node 3 is not None
    • Initialize mx = 0
    • Node 3 has 2 children: [5, 6]
    • For child 5: Call maxDepth(node 5)
  3. Call maxDepth(node 5) (leaf node):
    • Node 5 is not None
    • Initialize mx = 0
    • Node 5 has no children (empty list)
    • Return 1 + 0 = 1
  4. Back to maxDepth(node 3):
    • Update mx = max(0, 1) = 1
    • For child 6: Call maxDepth(node 6)
  5. Call maxDepth(node 6) (leaf node):
    • Node 6 is not None
    • Initialize mx = 0
    • Node 6 has no children
    • Return 1 + 0 = 1
  6. Back to maxDepth(node 3):
    • Update mx = max(1, 1) = 1
    • All children processed
    • Return 1 + 1 = 2
  7. Back to maxDepth(node 1):
    • Update mx = max(0, 2) = 2
    • For child 2: Call maxDepth(node 2)
  8. Call maxDepth(node 2) (leaf node):
    • Node 2 has no children
    • Return 1 + 0 = 1
  9. Back to maxDepth(node 1):
    • Update mx = max(2, 1) = 2
    • For child 4: Call maxDepth(node 4)
  10. Call maxDepth(node 4) (leaf node):
    • Node 4 has no children
    • Return 1 + 0 = 1
  11. Final step in maxDepth(node 1):
    • Update mx = max(2, 1) = 2
    • All children processed
    • Return 1 + 2 = 3

The algorithm correctly returns 3 as the maximum depth. Each node adds 1 to the maximum depth of its children, and leaf nodes contribute a depth of 1. The recursion naturally finds the longest path from root to any leaf.

Solution Implementation

1"""
2# Definition for a Node.
3class Node:
4    def __init__(self, val: Optional[int] = None, children: Optional[List['Node']] = None):
5        self.val = val
6        self.children = children
7"""
8
9from typing import Optional, List
10
11
12class Solution:
13    def maxDepth(self, root: 'Node') -> int:
14        """
15        Calculate the maximum depth of an N-ary tree.
16      
17        Args:
18            root: The root node of the N-ary tree
19          
20        Returns:
21            The maximum depth of the tree
22        """
23        # Base case: if the tree is empty, depth is 0
24        if root is None:
25            return 0
26      
27        # If the node has no children, it's a leaf node with depth 1
28        if not root.children:
29            return 1
30      
31        # Initialize variable to track maximum depth among all children
32        max_child_depth = 0
33      
34        # Recursively find the maximum depth among all children
35        for child in root.children:
36            child_depth = self.maxDepth(child)
37            max_child_depth = max(max_child_depth, child_depth)
38      
39        # Return current node's depth (1) plus the maximum child depth
40        return 1 + max_child_depth
41
1/*
2// Definition for a Node.
3class Node {
4    public int val;
5    public List<Node> children;
6
7    public Node() {}
8
9    public Node(int _val) {
10        val = _val;
11    }
12
13    public Node(int _val, List<Node> _children) {
14        val = _val;
15        children = _children;
16    }
17};
18*/
19
20class Solution {
21    /**
22     * Calculates the maximum depth of an n-ary tree.
23     * The depth is defined as the number of nodes along the longest path 
24     * from the root node down to the farthest leaf node.
25     * 
26     * @param root The root node of the n-ary tree
27     * @return The maximum depth of the tree
28     */
29    public int maxDepth(Node root) {
30        // Base case: if the tree is empty, return depth of 0
31        if (root == null) {
32            return 0;
33        }
34      
35        // Initialize variable to track the maximum depth among all children
36        int maxChildDepth = 0;
37      
38        // Recursively calculate the depth of each child subtree
39        for (Node child : root.children) {
40            // Update the maximum depth if current child's depth is greater
41            maxChildDepth = Math.max(maxChildDepth, maxDepth(child));
42        }
43      
44        // Return the maximum child depth plus 1 (for the current node)
45        return 1 + maxChildDepth;
46    }
47}
48
1/*
2// Definition for a Node.
3class Node {
4public:
5    int val;
6    vector<Node*> children;
7
8    Node() {}
9
10    Node(int _val) {
11        val = _val;
12    }
13
14    Node(int _val, vector<Node*> _children) {
15        val = _val;
16        children = _children;
17    }
18};
19*/
20
21class Solution {
22public:
23    /**
24     * Calculates the maximum depth of an N-ary tree
25     * @param root: The root node of the N-ary tree
26     * @return: The maximum depth of the tree
27     */
28    int maxDepth(Node* root) {
29        // Base case: empty tree has depth 0
30        if (!root) {
31            return 0;
32        }
33      
34        // Initialize variable to track maximum depth among all children
35        int maxChildDepth = 0;
36      
37        // Recursively find the maximum depth among all child subtrees
38        for (Node* child : root->children) {
39            maxChildDepth = max(maxChildDepth, maxDepth(child));
40        }
41      
42        // Return the maximum child depth plus 1 (for current node)
43        return maxChildDepth + 1;
44    }
45};
46
1/**
2 * Definition for _Node.
3 * class _Node {
4 *     val: number
5 *     children: _Node[]
6 *
7 *     constructor(val?: number, children?: _Node[]) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.children = (children===undefined ? [] : children)
10 *     }
11 * }
12 */
13
14/**
15 * Calculates the maximum depth of an n-ary tree
16 * @param root - The root node of the n-ary tree
17 * @returns The maximum depth of the tree (number of nodes along the longest path from root to leaf)
18 */
19function maxDepth(root: _Node | null): number {
20    // Base case: if the tree is empty, depth is 0
21    if (!root) {
22        return 0;
23    }
24  
25    // If the node has no children, it's a leaf node with depth 1
26    if (root.children.length === 0) {
27        return 1;
28    }
29  
30    // Recursively calculate the depth of each child subtree
31    const childDepths: number[] = root.children.map((child: _Node) => maxDepth(child));
32  
33    // The depth of current node is 1 plus the maximum depth among all children
34    const maxChildDepth: number = Math.max(...childDepths);
35  
36    return 1 + maxChildDepth;
37}
38

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the n-ary tree. Each node in the tree is visited exactly once. At each node, the algorithm performs a constant amount of work (comparing maximum values and making recursive calls), aside from the recursion itself. Since we visit all n nodes exactly once, the total time complexity is O(n), where n is the total number of nodes in the tree.

Space Complexity: O(n)

The space complexity is determined by the recursion call stack. In the worst case, the tree could be completely unbalanced (essentially a linked list 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 balanced tree, the recursion depth would be O(log n), but since we consider worst-case complexity, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Forgetting to Handle Empty Children List

A common mistake is not properly handling the case where root.children might be None instead of an empty list []. If the Node class implementation allows children to be None for leaf nodes, iterating over None will cause a TypeError.

Problematic Code:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    max_child_depth = 0
    # This will fail if root.children is None
    for child in root.children:
        max_child_depth = max(max_child_depth, self.maxDepth(child))
  
    return 1 + max_child_depth

Solution: Add a check to ensure children is not None before iterating:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    # Handle both None and empty list cases
    if not root.children:
        return 1
  
    max_child_depth = 0
    for child in root.children:
        max_child_depth = max(max_child_depth, self.maxDepth(child))
  
    return 1 + max_child_depth

Pitfall 2: Making Redundant Recursive Calls

Another mistake is calling self.maxDepth(child) multiple times for the same child, which significantly increases time complexity.

Problematic Code:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    mx = 0
    for child in root.children:
        # Calling maxDepth twice for each child!
        mx = max(mx, self.maxDepth(child))
        print(f"Depth of child: {self.maxDepth(child)}")  # Second call
  
    return 1 + mx

Solution: Store the result of the recursive call in a variable to avoid redundant computation:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    mx = 0
    for child in root.children:
        child_depth = self.maxDepth(child)  # Call once and store
        mx = max(mx, child_depth)
  
    return 1 + mx

Pitfall 3: Incorrectly Initializing the Maximum Depth

Some might initialize the maximum depth tracker to 1 instead of 0, leading to incorrect results when the node has children.

Problematic Code:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    # Wrong initialization - adds extra depth
    mx = 1  
    for child in root.children:
        mx = max(mx, self.maxDepth(child))
  
    return 1 + mx  # This would return 2 + actual_max_child_depth

Solution: Always initialize the maximum child depth to 0:

def maxDepth(self, root: 'Node') -> int:
    if root is None:
        return 0
  
    mx = 0  # Correct initialization
    for child in root.children:
        mx = max(mx, self.maxDepth(child))
  
    return 1 + mx
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More