Facebook Pixel

429. N-ary Tree Level Order Traversal

Problem Description

You are given an n-ary tree (a tree where each node can have any number of children, not just 2 like in a binary tree). Your task is to return the level order traversal of the tree's node values.

Level order traversal means visiting all nodes at the same depth level from left to right before moving to the next level. The result should be a list of lists, where each inner list contains all the values at a particular level of the tree.

For example, if you have a tree that looks like:

  • Level 0: Root node with value 1
  • Level 1: Children of root with values 3, 2, 4
  • Level 2: Children of the level 1 nodes

The level order traversal would return: [[1], [3, 2, 4], ...]

The tree is represented using a Node class where:

  • Each node has a val attribute storing its integer value
  • Each node has a children attribute which is a list of its child nodes

If the tree is empty (root is None), return an empty list.

The solution uses a breadth-first search (BFS) approach with a queue to traverse the tree level by level. For each level, it collects all node values in that level before moving to the next level.

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 each node can have multiple children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with an n-ary tree structure.

DFS?

  • Not quite: While DFS is suggested by the flowchart for trees, let's examine why BFS is actually the better choice here. The problem specifically asks for level order traversal, which means we need to visit all nodes at depth 0, then all nodes at depth 1, then depth 2, and so on. This is the defining characteristic of BFS traversal.

Why BFS over DFS:

  • Level-by-level processing: BFS naturally processes nodes level by level using a queue, which perfectly matches the requirement of level order traversal.
  • Result structure: The output needs to be grouped by levels [[level0], [level1], [level2], ...], which BFS handles elegantly by processing all nodes at the current level before moving to the next.
  • Order preservation: BFS visits nodes from left to right at each level, maintaining the required ordering.

While the flowchart suggests DFS for trees, the specific requirement of level order traversal makes BFS the optimal choice. The solution uses a queue to implement BFS, processing all nodes at each level together before moving to the next level.

Conclusion: The flowchart helps identify that we're dealing with a tree problem, but the specific requirement of level order traversal leads us to choose BFS (Breadth-First Search) as the appropriate algorithm.

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

Intuition

When we need to traverse a tree level by level, the key insight is that we want to visit all nodes at the same distance from the root before moving further down. Think of it like exploring a building floor by floor - you want to visit all rooms on floor 1 before going to floor 2.

The natural way to achieve this "spreading outward" pattern is using a queue. Why? Because a queue follows First-In-First-Out (FIFO) order. When we process a node, we add all its children to the back of the queue. This ensures that all nodes at the current level are processed before any nodes at the next level.

Here's the critical pattern: at any point in time, our queue contains nodes from at most 2 consecutive levels. When we start processing a level, the queue contains only nodes from that level. As we process each node, we add its children (next level nodes) to the back of the queue.

To separate nodes by levels in our result, we need to know how many nodes belong to the current level. The trick is to capture the queue's size at the beginning of each level - this tells us exactly how many nodes to process before moving to the next level. We process exactly that many nodes, collecting their values in a list, while adding their children to the queue for the next iteration.

This approach naturally handles the n-ary nature of the tree - whether a node has 0, 1, or many children doesn't matter. We simply add all children to the queue using q.extend(root.children), and the FIFO property of the queue ensures level-order traversal.

The edge case of an empty tree (root is None) is handled upfront by returning an empty list, avoiding any unnecessary queue operations.

Solution Approach

The solution implements BFS using a queue data structure to achieve level-order traversal. Let's walk through the implementation step by step:

1. Initial Setup and Edge Case Handling

ans = []
if root is None:
    return ans

We first create an empty result list ans to store our level-by-level traversal. If the root is None (empty tree), we return the empty list immediately.

2. Queue Initialization

q = deque([root])

We initialize a queue using Python's deque (double-ended queue) with the root node. The deque provides O(1) operations for adding and removing elements from both ends, making it perfect for implementing a queue.

3. Level-by-Level Processing

while q:
    t = []
    for _ in range(len(q)):

The outer while loop continues as long as there are nodes to process. For each level, we:

  • Create a temporary list t to store values of nodes at the current level
  • Capture the current queue size with len(q) - this tells us exactly how many nodes belong to the current level

4. Processing Nodes at Current Level

root = q.popleft()
t.append(root.val)
q.extend(root.children)

For each node at the current level:

  • Remove it from the front of the queue using popleft() (FIFO behavior)
  • Add its value to the current level's list t
  • Add all its children to the back of the queue using extend() - these will be processed in the next level

5. Collecting Results

ans.append(t)

After processing all nodes at the current level, we add the complete level list t to our result ans.

Time and Space Complexity:

  • Time Complexity: O(n) where n is the total number of nodes, as we visit each node exactly once
  • Space Complexity: O(n) for storing the result and O(w) for the queue, where w is the maximum width of the tree (maximum number of nodes at any level)

The beauty of this approach is its simplicity - the queue naturally maintains the level-order property, and by processing nodes in chunks (based on the queue size at each level's start), we can easily group nodes by their levels.

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 BFS level-order traversal works.

Consider this n-ary tree:

       1
     / | \
    3  2  4
   / \
  5   6

Initial State:

  • ans = [] (empty result list)
  • q = deque([Node(1)]) (queue contains only root)

Level 0 Processing:

  • Queue size at start: 1 (so we process 1 node)
  • t = [] (temporary list for this level)
  • Process Node(1):
    • popleft() removes Node(1) from queue
    • Add 1 to t: t = [1]
    • Add Node(1)'s children to queue: q = deque([Node(3), Node(2), Node(4)])
  • Add level to result: ans = [[1]]

Level 1 Processing:

  • Queue size at start: 3 (so we process 3 nodes)
  • t = [] (new temporary list)
  • Process Node(3):
    • popleft() removes Node(3)
    • Add 3 to t: t = [3]
    • Add children: q = deque([Node(2), Node(4), Node(5), Node(6)])
  • Process Node(2):
    • popleft() removes Node(2)
    • Add 2 to t: t = [3, 2]
    • No children to add
  • Process Node(4):
    • popleft() removes Node(4)
    • Add 4 to t: t = [3, 2, 4]
    • No children to add
  • Add level to result: ans = [[1], [3, 2, 4]]

Level 2 Processing:

  • Queue size at start: 2 (so we process 2 nodes)
  • t = [] (new temporary list)
  • Process Node(5):
    • popleft() removes Node(5)
    • Add 5 to t: t = [5]
    • No children to add
  • Process Node(6):
    • popleft() removes Node(6)
    • Add 6 to t: t = [5, 6]
    • No children to add
  • Add level to result: ans = [[1], [3, 2, 4], [5, 6]]

Termination:

  • Queue is now empty, while loop exits
  • Return ans = [[1], [3, 2, 4], [5, 6]]

The key insight: by capturing the queue size at the start of each level (1, then 3, then 2), we know exactly how many nodes to process before moving to the next level, even as we're adding new nodes to the queue.

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
7"""
8
9from collections import deque
10from typing import List, Optional
11
12
13class Solution:
14    def levelOrder(self, root: Optional['Node']) -> List[List[int]]:
15        """
16        Performs level-order traversal of an N-ary tree.
17      
18        Args:
19            root: The root node of the N-ary tree
20          
21        Returns:
22            A list of lists containing node values grouped by level
23        """
24        result = []
25      
26        # Handle empty tree case
27        if root is None:
28            return result
29      
30        # Initialize queue with root node for BFS traversal
31        queue = deque([root])
32      
33        # Process nodes level by level
34        while queue:
35            current_level = []
36            level_size = len(queue)
37          
38            # Process all nodes at the current level
39            for _ in range(level_size):
40                # Remove and process the front node
41                node = queue.popleft()
42                current_level.append(node.val)
43              
44                # Add all children of current node to queue for next level
45                if node.children:
46                    queue.extend(node.children)
47          
48            # Add current level's values to result
49            result.append(current_level)
50      
51        return result
52
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     * Performs level order traversal on an N-ary tree.
23     * @param root The root node of the N-ary tree
24     * @return A list of lists containing node values grouped by level
25     */
26    public List<List<Integer>> levelOrder(Node root) {
27        // Initialize result list to store nodes at each level
28        List<List<Integer>> result = new ArrayList<>();
29      
30        // Handle edge case: empty tree
31        if (root == null) {
32            return result;
33        }
34      
35        // Initialize queue for BFS traversal
36        Deque<Node> queue = new ArrayDeque<>();
37        queue.offer(root);
38      
39        // Process nodes level by level
40        while (!queue.isEmpty()) {
41            // List to store current level's node values
42            List<Integer> currentLevel = new ArrayList<>();
43          
44            // Get the number of nodes at current level
45            int levelSize = queue.size();
46          
47            // Process all nodes at current level
48            for (int i = 0; i < levelSize; i++) {
49                // Remove and process the front node
50                Node currentNode = queue.poll();
51                currentLevel.add(currentNode.val);
52              
53                // Add all children of current node to queue for next level
54                queue.addAll(currentNode.children);
55            }
56          
57            // Add current level's values to result
58            result.add(currentLevel);
59        }
60      
61        return result;
62    }
63}
64
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    vector<vector<int>> levelOrder(Node* root) {
24        // Result vector to store nodes at each level
25        vector<vector<int>> result;
26      
27        // Handle empty tree case
28        if (root == nullptr) {
29            return result;
30        }
31      
32        // Initialize BFS queue with root node
33        queue<Node*> nodeQueue;
34        nodeQueue.push(root);
35      
36        // Process nodes level by level
37        while (!nodeQueue.empty()) {
38            // Vector to store current level's node values
39            vector<int> currentLevel;
40          
41            // Get the number of nodes at current level
42            int levelSize = nodeQueue.size();
43          
44            // Process all nodes at current level
45            for (int i = 0; i < levelSize; ++i) {
46                // Get and remove front node from queue
47                Node* currentNode = nodeQueue.front();
48                nodeQueue.pop();
49              
50                // Add current node's value to current level
51                currentLevel.push_back(currentNode->val);
52              
53                // Add all children to queue for next level processing
54                for (Node* child : currentNode->children) {
55                    nodeQueue.push(child);
56                }
57            }
58          
59            // Add current level to result
60            result.push_back(currentLevel);
61        }
62      
63        return result;
64    }
65};
66
1/**
2 * Definition for node.
3 * class Node {
4 *     val: number
5 *     children: Node[]
6 *     constructor(val?: number) {
7 *         this.val = (val===undefined ? 0 : val)
8 *         this.children = []
9 *     }
10 * }
11 */
12
13/**
14 * Performs level-order traversal of an N-ary tree and returns values grouped by level
15 * @param root - The root node of the N-ary tree
16 * @returns A 2D array where each sub-array contains values from the same level
17 */
18function levelOrder(root: Node | null): number[][] {
19    // Initialize result array to store values by level
20    const result: number[][] = [];
21  
22    // Handle empty tree case
23    if (!root) {
24        return result;
25    }
26  
27    // Initialize queue with root node for BFS traversal
28    const queue: Node[] = [root];
29  
30    // Process nodes level by level
31    while (queue.length > 0) {
32        // Store nodes for the next level
33        const nextLevelNodes: Node[] = [];
34        // Store values of current level
35        const currentLevelValues: number[] = [];
36      
37        // Process all nodes in the current level
38        for (const node of queue) {
39            // Add all children to next level queue
40            nextLevelNodes.push(...node.children);
41            // Collect current node's value
42            currentLevelValues.push(node.val);
43        }
44      
45        // Add current level values to result
46        result.push(currentLevelValues);
47      
48        // Replace queue contents with next level nodes
49        queue.splice(0, queue.length, ...nextLevelNodes);
50    }
51  
52    return result;
53}
54

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a breadth-first search (BFS) traversal of the N-ary tree. Each node is visited exactly once - it's added to the queue once when discovered and processed once when dequeued. For each node, we perform constant time operations: appending its value to the temporary list t and extending the queue with its children. Since we visit all n nodes exactly once and perform O(1) operations for each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity has two main components:

  1. Queue space: In the worst case, the queue can hold all nodes at the widest level of the tree. For a balanced N-ary tree, this could be up to O(n) nodes (consider a tree where the last level contains more than half of all nodes).
  2. Output space: The ans list stores all n node values organized by levels, requiring O(n) space.
  3. Temporary list: The list t holds nodes at the current level, but its space is already accounted for as it becomes part of ans.

Therefore, the overall space complexity is O(n), where n is the total number of nodes in the N-ary tree.

Common Pitfalls

1. Incorrectly Processing Multiple Levels Together

One of the most common mistakes is failing to properly separate nodes by their levels. If you don't capture the queue size at the beginning of each level's processing, you'll mix nodes from different levels together.

Incorrect approach:

while queue:
    node = queue.popleft()
    result.append(node.val)  # This would mix all levels together
    queue.extend(node.children)

Why it fails: Without tracking how many nodes belong to each level, you can't group them correctly. The queue will contain nodes from multiple levels simultaneously.

Solution: Always capture level_size = len(queue) before processing each level and use a loop to process exactly that many nodes:

while queue:
    current_level = []
    level_size = len(queue)  # Capture size BEFORE processing
    for _ in range(level_size):
        node = queue.popleft()
        current_level.append(node.val)
        queue.extend(node.children)
    result.append(current_level)

2. Not Handling None/Empty Children Lists

Another pitfall is assuming node.children always exists and is non-empty, which can lead to errors when extending the queue.

Problematic code:

queue.extend(node.children)  # Fails if children is None

Solution: Add a safety check before extending:

if node.children:  # Check if children exists and is not empty
    queue.extend(node.children)

3. Using Wrong Queue Operations

Using append() and pop() instead of append() and popleft() (or equivalently, using a list instead of deque) can accidentally implement a stack instead of a queue, resulting in depth-first traversal rather than breadth-first.

Incorrect (DFS instead of BFS):

queue = [root]
while queue:
    node = queue.pop()  # LIFO - takes from the end!
    # ... process node
    queue.extend(node.children)

Solution: Use deque with popleft() for proper FIFO behavior:

from collections import deque
queue = deque([root])
while queue:
    node = queue.popleft()  # FIFO - takes from the front

4. Modifying the Queue Size During Level Processing

A subtle bug occurs if you try to use the queue's length dynamically within the level-processing loop, as the queue size changes when you add children.

Incorrect:

while queue:
    current_level = []
    for _ in range(len(queue)):  # Don't recalculate len(queue) each iteration!
        node = queue.popleft()
        current_level.append(node.val)
        queue.extend(node.children)
        # len(queue) has now changed!

While Python's range() captures the value once at the beginning, relying on this behavior in other contexts or languages could cause issues.

Best Practice: Always store the level size in a variable before processing:

level_size = len(queue)
for _ in range(level_size):
    # Process exactly level_size nodes
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

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

Load More