Facebook Pixel

1522. Diameter of N-Ary Tree 🔒

Problem Description

You are given the root of 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 diameter of this tree.

The diameter of an N-ary tree is defined as the length of the longest path between any two nodes in the tree. This path is measured by counting the number of edges between the two nodes.

Key points to understand:

  • The longest path may or may not pass through the root node
  • The path connects any two nodes in the tree
  • The length is counted as the number of edges, not the number of nodes
  • An N-ary tree node has a value and a list of children (which can be empty for leaf nodes)

For example, if you have a tree where the longest path goes from one leaf node, up through some parent nodes, and then down to another leaf node spanning 5 edges total, then the diameter would be 5.

The tree structure is defined with a Node class where:

  • val represents the node's value
  • children is a list containing all child nodes

The input tree is serialized using level-order traversal, where groups of children are separated by null values, but you'll be working directly with the Node object structure.

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, and there's a hierarchical structure with a root node and children.

Is it a tree?

  • Yes: The problem explicitly states we're working with an N-ary tree structure, which is a connected acyclic graph with a root node where each node can have multiple children.

DFS

  • We arrive at DFS (Depth-First Search) as our algorithm choice.

Conclusion: The flowchart suggests using a DFS approach for finding the diameter of the N-ary tree.

This makes perfect sense because:

  1. We need to explore the depth of each subtree to find the longest paths
  2. For each node, we need to calculate the height of all its subtrees
  3. The diameter at any node could be the sum of the two longest paths among its children
  4. DFS allows us to process nodes bottom-up, calculating heights and updating the diameter as we traverse back up the tree

The DFS pattern is ideal here as it naturally computes the height of subtrees while simultaneously tracking the maximum diameter found so far by considering paths that pass through each node.

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

Intuition

To find the diameter of an N-ary tree, we need to think about what constitutes the longest path between any two nodes. The key insight is that the longest path in a tree will always be between two leaf nodes (or a leaf and another node), and this path will have a "peak" - a highest node where the path changes direction from going up to going down.

For any node in the tree, the longest path that passes through it would be formed by:

  1. Going down to the deepest leaf in one of its subtrees
  2. Going down to the deepest leaf in another one of its subtrees

This means at each node, we need to know the heights of all its subtrees. The diameter passing through that node would be the sum of the two largest heights among its children.

Here's the critical observation: as we traverse the tree using DFS, we can:

  • Calculate the height of each subtree (which is 1 + maximum height among all children)
  • Keep track of the two largest heights (m1 and m2) among all children at each node
  • Update our global maximum diameter with m1 + m2 at each node

Why do we need only the top two heights? Because a path can only go down through at most two different children of any node - one path going down through one child and another path going down through a different child.

The beauty of this approach is that with a single DFS traversal, we simultaneously:

  • Compute heights bottom-up (needed for parent calculations)
  • Track the maximum diameter seen so far (our answer)

Each node returns its height to its parent while potentially updating the global diameter, making this an efficient O(n) solution where we visit each node exactly once.

Learn more about Tree and Depth-First Search patterns.

Solution Approach

The implementation uses a recursive DFS approach with the following key components:

Main Structure:

  • We define a helper function dfs(root) that returns the height of the subtree rooted at the given node
  • We maintain a global variable ans to track the maximum diameter found so far

DFS Function Logic:

  1. Base Case: If the node is None, return height 0

  2. Process Each Child:

    • We maintain two variables m1 and m2 to track the two largest heights among all children
    • m1 stores the maximum height, m2 stores the second maximum height
    • Initially, both are set to 0
  3. Height Collection and Tracking:

    for child in root.children:
        t = dfs(child)  # Get height of child's subtree
        if t > m1:
            m2, m1 = m1, t  # New maximum found, shift m1 to m2
        elif t > m2:
            m2 = t  # New second maximum found

    This clever updating ensures we always have the top two heights without needing to sort or use additional space.

  4. Update Global Diameter:

    ans = max(ans, m1 + m2)

    The diameter passing through the current node is the sum of the two longest paths going down through its children.

  5. Return Height:

    return 1 + m1

    The height of the current subtree is 1 (for the current node) plus the maximum height among all its children.

Key Insights:

  • The nonlocal ans declaration allows the nested function to modify the outer scope's ans variable
  • The algorithm visits each node exactly once, giving us O(n) time complexity
  • Space complexity is O(h) where h is the height of the tree, due to the recursion call stack

The elegance of this solution lies in how it simultaneously computes two things: the height of each subtree (needed for parent calculations) and the maximum diameter (our final answer), all in a single tree traversal.

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 small N-ary tree example to illustrate how the solution works:

        1
       /|\
      3 2 4
     / \
    5   6

In this tree:

  • Node 1 has three children: 3, 2, and 4
  • Node 3 has two children: 5 and 6
  • Nodes 2, 4, 5, and 6 are leaf nodes

Step-by-step DFS traversal:

  1. Start DFS from root (node 1)
    • Process child 3:
      • Process child 5 (leaf): returns height 1
      • Process child 6 (leaf): returns height 1
      • At node 3: m1=1, m2=1
      • Update diameter: ans = max(0, 1+1) = 2
      • Return height: 1 + 1 = 2
  2. Back at root (node 1), continue with child 2:
    • Node 2 (leaf): returns height 1
    • Current state at root: m1=2 (from node 3), m2=1 (from node 2)
  3. Process child 4:
    • Node 4 (leaf): returns height 1
    • Current state at root: m1=2, m2=1 (node 4's height doesn't change m2)
  4. Final calculation at root:
    • m1=2 (height through node 3), m2=1 (height through node 2 or 4)
    • Update diameter: ans = max(2, 2+1) = 3
    • Return height: 1 + 2 = 3

Result: The diameter is 3, representing the longest path from node 5 → 3 → 1 → 2 (or 4), which has 3 edges.

The algorithm correctly identifies that the longest path passes through the root, connecting the deepest leaf in the left subtree (node 5 or 6) with a leaf in another subtree (node 2 or 4). The key insight is how at each node, we track the two largest heights among children and update the global diameter with their sum.

Solution Implementation

1"""
2# Definition for a Node.
3class Node:
4    def __init__(self, val=None, children=None):
5        self.val = val
6        self.children = children if children is not None else []
7"""
8
9
10class Solution:
11    def diameter(self, root: 'Node') -> int:
12        """
13        Find the diameter of an N-ary tree.
14        The diameter is the longest path between any two nodes in the tree.
15      
16        :type root: 'Node'
17        :rtype: int
18        """
19      
20        def dfs(node: 'Node') -> int:
21            """
22            Depth-first search to find the maximum depth from current node.
23            Also updates the global diameter during traversal.
24          
25            :param node: Current node being processed
26            :return: Maximum depth from this node to any leaf
27            """
28            if node is None:
29                return 0
30          
31            # Track the two longest paths from current node through its children
32            max_depth_1 = 0  # Longest path
33            max_depth_2 = 0  # Second longest path
34          
35            # Process each child and find the two longest paths
36            for child in node.children:
37                child_depth = dfs(child)
38              
39                if child_depth > max_depth_1:
40                    # Found a new longest path, shift previous longest to second
41                    max_depth_2 = max_depth_1
42                    max_depth_1 = child_depth
43                elif child_depth > max_depth_2:
44                    # Found a new second longest path
45                    max_depth_2 = child_depth
46          
47            # Update global diameter: sum of two longest paths through current node
48            nonlocal max_diameter
49            max_diameter = max(max_diameter, max_depth_1 + max_depth_2)
50          
51            # Return the depth from current node (1 + longest path to leaf)
52            return 1 + max_depth_1
53      
54        # Initialize the maximum diameter
55        max_diameter = 0
56      
57        # Start DFS from root
58        dfs(root)
59      
60        return max_diameter
61
1/*
2// Definition for a Node.
3class Node {
4    public int val;
5    public List<Node> children;
6
7    public Node() {
8        children = new ArrayList<Node>();
9    }
10
11    public Node(int _val) {
12        val = _val;
13        children = new ArrayList<Node>();
14    }
15
16    public Node(int _val,ArrayList<Node> _children) {
17        val = _val;
18        children = _children;
19    }
20};
21*/
22
23class Solution {
24    // Variable to store the maximum diameter found
25    private int maxDiameter;
26
27    /**
28     * Calculates the diameter of an N-ary tree.
29     * The diameter is the longest path between any two nodes in the tree.
30     * 
31     * @param root The root node of the N-ary tree
32     * @return The diameter of the tree
33     */
34    public int diameter(Node root) {
35        maxDiameter = 0;
36        calculateHeight(root);
37        return maxDiameter;
38    }
39
40    /**
41     * Performs depth-first search to calculate the height of each subtree
42     * while updating the maximum diameter.
43     * 
44     * @param node The current node being processed
45     * @return The height of the subtree rooted at the current node
46     */
47    private int calculateHeight(Node node) {
48        // Base case: null node has height 0
49        if (node == null) {
50            return 0;
51        }
52      
53        // Track the two largest heights among all children
54        int largestHeight = 0;
55        int secondLargestHeight = 0;
56      
57        // Process each child and update the two largest heights
58        for (Node child : node.children) {
59            int childHeight = calculateHeight(child);
60          
61            if (childHeight > largestHeight) {
62                // New largest height found, shift previous largest to second
63                secondLargestHeight = largestHeight;
64                largestHeight = childHeight;
65            } else if (childHeight > secondLargestHeight) {
66                // New second largest height found
67                secondLargestHeight = childHeight;
68            }
69        }
70      
71        // Update the maximum diameter: sum of two longest paths through current node
72        maxDiameter = Math.max(maxDiameter, largestHeight + secondLargestHeight);
73      
74        // Return height of current subtree (1 + height of tallest child)
75        return 1 + largestHeight;
76    }
77}
78
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    int maxDiameter;  // Stores the maximum diameter found
24  
25    /**
26     * Calculates the diameter of an N-ary tree
27     * The diameter is the length of the longest path between any two nodes
28     * @param root: Root node of the N-ary tree
29     * @return: The diameter of the tree
30     */
31    int diameter(Node* root) {
32        maxDiameter = 0;
33        calculateHeight(root);
34        return maxDiameter;
35    }
36  
37private:
38    /**
39     * DFS helper function to calculate the height of each subtree
40     * While calculating height, also updates the maximum diameter
41     * @param node: Current node being processed
42     * @return: Height of the subtree rooted at current node
43     */
44    int calculateHeight(Node* node) {
45        // Base case: null node has height 0
46        if (!node) {
47            return 0;
48        }
49      
50        // Track the two longest paths from children
51        int longestPath = 0;
52        int secondLongestPath = 0;
53      
54        // Process all children to find the two longest paths
55        for (Node* child : node->children) {
56            int childHeight = calculateHeight(child);
57          
58            // Update the two longest paths
59            if (childHeight > longestPath) {
60                // New longest path found, previous longest becomes second longest
61                secondLongestPath = longestPath;
62                longestPath = childHeight;
63            } else if (childHeight > secondLongestPath) {
64                // Update second longest path
65                secondLongestPath = childHeight;
66            }
67        }
68      
69        // Update maximum diameter: sum of two longest paths through current node
70        maxDiameter = max(maxDiameter, longestPath + secondLongestPath);
71      
72        // Return height of current subtree (1 + longest path from children)
73        return 1 + longestPath;
74    }
75};
76
1// Definition for a Node
2class Node {
3    val: number;
4    children: Node[];
5  
6    constructor(val?: number, children?: Node[]) {
7        this.val = (val === undefined ? 0 : val);
8        this.children = (children === undefined ? [] : children);
9    }
10}
11
12// Global variable to store the maximum diameter found
13let maxDiameter: number;
14
15/**
16 * Calculates the diameter of an N-ary tree
17 * The diameter is the length of the longest path between any two nodes
18 * @param root - Root node of the N-ary tree
19 * @return The diameter of the tree
20 */
21function diameter(root: Node | null): number {
22    // Initialize the maximum diameter to 0
23    maxDiameter = 0;
24  
25    // Start DFS traversal to calculate heights and update diameter
26    calculateHeight(root);
27  
28    // Return the maximum diameter found
29    return maxDiameter;
30}
31
32/**
33 * DFS helper function to calculate the height of each subtree
34 * While calculating height, also updates the maximum diameter
35 * @param node - Current node being processed
36 * @return Height of the subtree rooted at current node
37 */
38function calculateHeight(node: Node | null): number {
39    // Base case: null node has height 0
40    if (!node) {
41        return 0;
42    }
43  
44    // Track the two longest paths from children
45    let longestPath: number = 0;
46    let secondLongestPath: number = 0;
47  
48    // Process all children to find the two longest paths
49    for (const child of node.children) {
50        // Recursively calculate the height of each child subtree
51        const childHeight: number = calculateHeight(child);
52      
53        // Update the two longest paths
54        if (childHeight > longestPath) {
55            // New longest path found, previous longest becomes second longest
56            secondLongestPath = longestPath;
57            longestPath = childHeight;
58        } else if (childHeight > secondLongestPath) {
59            // Update second longest path only
60            secondLongestPath = childHeight;
61        }
62    }
63  
64    // Update maximum diameter: sum of two longest paths through current node
65    // This represents the longest path that passes through the current node
66    maxDiameter = Math.max(maxDiameter, longestPath + secondLongestPath);
67  
68    // Return height of current subtree (1 + longest path from children)
69    // Height includes the current node plus the longest path below it
70    return 1 + longestPath;
71}
72

Time and Space Complexity

Time Complexity: O(n) where n is the total number of nodes in the N-ary tree.

The algorithm performs a depth-first search (DFS) traversal of the tree, visiting each node exactly once. At each node, we iterate through all its children to compute the two longest paths. While the number of children per node can vary, each edge in the tree is traversed exactly once (from parent to child). Since a tree with n nodes has n-1 edges, and we perform constant-time operations for updating m1, m2, and ans at each node, the overall time complexity is O(n).

Space Complexity: O(h) where h is the height of the tree.

The space complexity is determined by the recursive call stack during the DFS traversal. In the worst case, when the tree is completely unbalanced (resembling a linked list), the height h equals n, resulting in O(n) space complexity. In the best case, when the tree is balanced, the height would be O(log n) for a binary tree structure, though for an N-ary tree with varying branching factors, this can differ. The algorithm uses only a constant amount of extra space for variables (ans, m1, m2, t) at each recursive level, so the dominant factor is the recursion stack depth.

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

Common Pitfalls

1. Confusing Height vs. Diameter Calculation

Pitfall: A common mistake is thinking that the diameter is simply the height of the tree or twice the height. Developers often return 2 * height or just the height instead of properly tracking the sum of the two longest paths through each node.

Wrong approach:

def diameter(self, root):
    def dfs(node):
        if not node:
            return 0
        max_height = 0
        for child in node.children:
            max_height = max(max_height, dfs(child))
        return 1 + max_height
  
    return dfs(root)  # Wrong! This returns height, not diameter

Solution: Remember that diameter is the longest path between ANY two nodes, which might pass through any intermediate node, not necessarily the root. Always track max_depth_1 + max_depth_2 at each node.

2. Forgetting to Handle Trees with 0 or 1 Child

Pitfall: When a node has only one child or no children, the logic for finding two maximum heights can fail if not handled properly. Some implementations assume there are always at least two children.

Wrong approach:

for child in node.children:
    heights.append(dfs(child))
heights.sort(reverse=True)
max_diameter = max(max_diameter, heights[0] + heights[1])  # IndexError if < 2 children

Solution: Initialize both max_depth_1 and max_depth_2 to 0. This naturally handles cases with fewer than 2 children since unused variables remain 0.

3. Using Global Variables Incorrectly

Pitfall: Forgetting the nonlocal keyword when modifying the outer scope variable, or accidentally creating a local variable with the same name.

Wrong approach:

def diameter(self, root):
    max_diameter = 0
  
    def dfs(node):
        # Missing 'nonlocal' declaration
        max_diameter = max(max_diameter, m1 + m2)  # UnboundLocalError

Solution: Always use nonlocal max_diameter inside the nested function before modifying it, or use a mutable container like a list: max_diameter = [0] and update with max_diameter[0] = max(...).

4. Incorrect Edge Counting

Pitfall: Counting nodes instead of edges when calculating the diameter, leading to an off-by-one error.

Wrong approach:

# Returning node count instead of edge count
return 2 + max_depth_1 + max_depth_2  # Wrong! Adds extra nodes

Solution: The diameter is the number of edges, not nodes. When combining two paths through a node, simply add them: max_depth_1 + max_depth_2. Each path already represents the edge count from that node downward.

5. Not Updating max_depth_2 Correctly

Pitfall: Only tracking the maximum depth and forgetting to properly maintain the second maximum, or updating them in the wrong order.

Wrong approach:

for child in node.children:
    child_depth = dfs(child)
    if child_depth > max_depth_1:
        max_depth_1 = child_depth  # Lost the previous max_depth_1!
    elif child_depth > max_depth_2:
        max_depth_2 = child_depth

Solution: When finding a new maximum, always shift the old maximum to become the second maximum before updating: max_depth_2, max_depth_1 = max_depth_1, child_depth.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More