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.
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 EvaluatorExample 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:
- 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). - Output space: The
ans
list stores alln
node values organized by levels, requiringO(n)
space. - Temporary list: The list
t
holds nodes at the current level, but its space is already accounted for as it becomes part ofans
.
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
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!