429. N-ary Tree Level Order Traversal


Problem Description

The problem is to perform a level order traversal on an n-ary tree and return its nodes' values in a particular format. An n-ary tree is a tree in which each node can have 0 or more children, in contrast with a binary tree where each node has at most 2 children.

In level order traversal, we visit all nodes at the current depth level before moving on to nodes at the next depth level. This means starting at the root (top level of the tree) and visiting nodes level by level, moving from left to right, and proceeding downwards through the tree.

Input serialization for the tree is given in level order format. Each group of children is separated with a null value to indicate the end of one level and the beginning of the next level.

The goal here is to traverse the tree in level order and return a list of lists, where each sublist contains the values of nodes at each depth level of the tree.

Intuition

The intuition behind solving this problem is based on Breadth-First Search (BFS), a common algorithm that is adept at exploring the nodes level by level. The BFS algorithm uses a queue to keep track of the nodes to be visited next. We start with the root node and then continue with the children of each node, following the level order.

Here's the step-by-step approach to how we can arrive at the solution:

  1. Initialize an empty list ans to store the final list of lists of node values.
  2. If the root is None, the tree is empty, and we return an empty list.
  3. Initialize a queue q with the root node.
  4. As long as the queue is not empty, we:
    • Initialize a temporary list t to hold node values for the current level.
    • Calculate the number of nodes at the current level, equivalent to the queue's current size.
    • Dequeue each node at the current level (using the stored size to limit iterations).
    • Add its value to the temporary list t.
    • Add all its children to the queue.
    • At the end of the level, append t to ans.
  5. Return ans.

The breadth-first nature of the algorithm ensures that we visit all nodes at the current level before any nodes at the next level are visited. By extending the queue with the children of each node, we are effectively lining them up for the next round of visits, which corresponds to the next level in the tree.

This solution traverses the tree in an optimal way for level order traversal, ensuring that each node's value is added to the list in the correct order.

Learn more about Tree and Breadth-First Search patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Which data structure is used in a depth first search?

Solution Approach

The implementation of the level order traversal is realized through the use of a Breadth-First Search (BFS) strategy. Let's walk through the important parts of the implementation which tie back to the BFS algorithm:

  1. Queue with Deque: The Python deque from the collections module is used as a queue. It allows for appending and popping elements efficiently from both ends, which is essential for BFS. Unlike a list, a deque has O(1) time complexity for append and pop operations on either end.

  2. Outer While Loop: This loop continues as long as there are nodes to process in the queue q:

    1while q:

    The presence of nodes in the queue indicates that there are more levels in the tree left to explore.

  3. Inner Loop with Fixed Size:

    1for _ in range(len(q)):

    The inner loop iterates exactly the number of times equal to the elements present at the start of the iteration, which corresponds to the current level's width (or number of nodes at that level).

  4. Node Processing:

    1root = q.popleft()
    2t.append(root.val)

    The currently visited node is popped from the front of the queue, and its value is added to a temporary list t that stores all node values from the current level.

  5. Children Enqueueing:

    1q.extend(root.children)

    The children of the current node are added to the queue. They will be processed once all nodes of the current level are processed, ensuring the level order.

  6. Appending Level Values to Answer:

    1ans.append(t)

    After processing all nodes of the current level, the list t containing their values is appended to the answer list ans.

By following this approach, the tree is traversed level by level, with each level's nodes' values being captured in their respective sublists within the ans list. The BFS algorithm is well-suited for this kind of traversal by its inherent nature of visiting nodes in a breadthward manner, which perfectly aligns with the requirements of level order traversal. This makes BFS both an intuitive and efficient solution for this problem.

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

Depth first search can be used to find whether two components in a graph are connected.

Example Walkthrough

Let's use a simple example to illustrate how the solution approach is applied. Consider an n-ary tree that looks like this:

1     1
2   / | \
3  2  3  4
4     |
5     5

The tree has one root node (1), which has three children (2, 3, 4). Node 3 further has one child (5). We will walk through the level order traversal of this tree using our solution approach.

  1. Initialize an empty list ans to hold the results and a queue q to keep track of nodes to visit. Start by adding the root node to the queue.

  2. Since the root node is not None, put the root node in the queue. The queue q initially holds [1].

  3. Enter the outer loop, as the queue is not empty.

  4. Inner loop for current level (Level 0):

    • Calculate the current level size; there's one root node, so size is 1.
    • Initialize an empty list t.
    • Dequeue the first (and only) node, which is the root node with value 1.
    • Add the root node's value to t, resulting in t = [1].
    • Enqueue all children of the root node, so the queue q becomes [2, 3, 4].
    • Append t to ans, resulting in ans = [[1]].
  5. Continue the outer loop, as the queue still has elements.

  6. Inner loop for current level (Level 1):

    • Calculate the current level size; there are three nodes, so size is 3.
    • Reset list t to empty.
    • Dequeue and process three nodes in order: 2, 3, and 4.
    • Add their values to t as each is processed, resulting in t = [2, 3, 4].
    • Enqueue the children of these three nodes as they are visited. Only node 3 has a child (5), so now the queue q is [5].
    • Append t to ans, giving us ans = [[1], [2, 3, 4]].
  7. Proceed with the loop since the queue still has one element (5).

  8. Inner loop for current level (Level 2):

    • There's one node at this level; the list t is reset to empty.
    • Dequeue node 5, add its value to t resulting in t = [5]. Node 5 has no children, so the queue remains empty after this.
    • Append t to ans to get ans = [[1], [2, 3, 4], [5]].
  9. The outer loop ends as there are no more nodes to process in the queue.

  10. Return the list ans.

The final output for the level order traversal of this small n-ary tree is [[1], [2, 3, 4], [5]], which clearly demonstrates the nodes at each level of the tree.

Solution Implementation

1from collections import deque
2from typing import List
3
4# Definition for a Node in an N-ary tree.
5class Node:
6    def __init__(self, val=None, children=None):
7        self.val = val
8        # Initialize children as an empty list if none are provided.
9        self.children = children if children is not None else []
10
11class Solution:
12    def levelOrder(self, root: Node) -> List[List[int]]:
13        # This list will hold the list of values at each level of the tree.
14        levels_list = []
15      
16        # If the root is None, there's nothing to traverse.
17        if root is None:
18            return levels_list
19      
20        # Initialize a deque with root for the level order traversal.
21        queue = deque([root])
22      
23        # Iterate as long as there are nodes to process.
24        while queue:
25            # Holds node values at the current level.
26            level_values = []
27          
28            # Iterate over all nodes at the current level.
29            for _ in range(len(queue)):
30                # Pop the leftmost node from the queue.
31                current_node = queue.popleft()
32                # Add its value to the level's list of values.
33                level_values.append(current_node.val)
34                # Extend the queue with the current node's children.
35                queue.extend(current_node.children)
36          
37            # Append the current level's values to the levels list.
38            levels_list.append(level_values)
39      
40        # Return the list of levels, with each level's values in a nested list.
41        return levels_list
42
1// Class definition including the levelOrder method to perform level order traversal on n-ary trees.
2class Solution {
3    // Method to traverse the tree in level order and return the result as a list of lists of integers.
4    public List<List<Integer>> levelOrder(Node root) {
5        List<List<Integer>> result = new ArrayList<>(); // Initialize a list to store the level order traversal result.
6      
7        // Early return if the root is null, meaning the tree is empty.
8        if (root == null) {
9            return result;
10        }
11      
12        Deque<Node> queue = new ArrayDeque<>(); // Initialize a queue to hold the nodes to visit.
13        queue.offer(root); // Place the root node in the queue.
14      
15        // Loop as long as the queue isn't empty.
16        while (!queue.isEmpty()) {
17            List<Integer> level = new ArrayList<>(); // Initialize a list to store the values at the current level.
18          
19            // Determine the number of nodes at this level.
20            int levelSize = queue.size();
21          
22            // Process each node on the current level.
23            for (int i = 0; i < levelSize; i++) {
24                Node currentNode = queue.poll(); // Remove the next node from the queue.
25                level.add(currentNode.val); // Add the node's value to the list for this level.
26              
27                // Add all of the current node's children to the queue.
28                for (Node child : currentNode.children) {
29                    queue.offer(child);
30                }
31            }
32          
33            // Add the current level's list of values to the result list.
34            result.add(level);
35        }
36      
37        return result; // Return the result list containing all levels' worth of values.
38    }
39}
40
41// Definition of the Node class that the solution expects to use.
42class Node {
43    public int val; // Value stored in the node.
44    public List<Node> children; // List of child nodes.
45
46    // Constructors for the Node class.
47    public Node() {}
48
49    public Node(int val) {
50        this.val = val;
51    }
52
53    public Node(int val, List<Node> children) {
54        this.val = val;
55        this.children = children;
56    }
57}
58
1#include <vector>  // Include the necessary header for std::vector
2#include <queue>   // Include the necessary header for std::queue
3
4// Definition for a Node. (Do not modify this part)
5class Node {
6public:
7    int val;  // Value of the node
8    vector<Node*> children;  // Vector of pointers to child nodes
9
10    // Constructor initializes an empty node
11    Node() {}
12
13    // Constructor initializes a node with a given value
14    Node(int _val) {
15        val = _val;
16    }
17
18    // Constructor initializes a node with a given value and children
19    Node(int _val, vector<Node*> _children) {
20        val = _val;
21        children = _children;
22    }
23};
24
25class Solution {
26public:
27    // This function performs a level order traversal on an n-ary tree
28    vector<vector<int>> levelOrder(Node* root) {
29        vector<vector<int>> result;  // To store the level order traversal
30        if (root == nullptr) 
31            return result;  // If the tree is empty, return an empty vector
32
33        // Queue to assist with BFS (Breadth-First Search)
34        queue<Node*> nodesQueue;
35        nodesQueue.push(root);  // Start with the root node
36
37        // Continue the search as long as there are nodes remaining
38        while (!nodesQueue.empty()) {
39            vector<int> level;  // To store the current level's values
40            int levelNodeCount = nodesQueue.size();  // Number of nodes on this level
41
42            // Iterate over all nodes of the current level
43            for (int i = 0; i < levelNodeCount; ++i) {
44                Node* currentNode = nodesQueue.front();  // Get the next node
45                nodesQueue.pop();
46                level.push_back(currentNode->val);  // Add the node's value to current level
47
48                // Add all the children of the current node to the queue
49                for (Node* child : currentNode->children) {
50                    nodesQueue.push(child);
51                }
52            }
53
54            result.push_back(level);  // Add the current level to the result
55        }
56      
57        return result;  // Return the completed level order traversal
58    }
59};
60
1// Type definition for a tree node with variable number of children.
2interface ITreeNode {
3  val: number;
4  children: ITreeNode[];
5}
6
7// Function to perform level order traversal of a tree and returns a list of values on each level.
8// @param root - The root node of the tree.
9// @returns - A two dimensional array with each sub-array representing a level in the tree.
10function levelOrderTraversal(root: ITreeNode | null): number[][] {
11  // Resultant array which will contain the level order traversal
12  const result: number[][] = [];
13
14  // If the tree is empty, return the empty array.
15  if (root === null) {
16    return result;
17  }
18
19  // Initialize queue for level order traversal
20  const queue: ITreeNode[] = [root];
21
22  // Iterate as long as there are nodes in the queue
23  while (queue.length > 0) {
24    // Number of nodes at the current level
25    const levelNodeCount = queue.length;
26  
27    // Array to hold values of the current level nodes
28    const levelValues: number[] = [];
29
30    for (let i = 0; i < levelNodeCount; i++) {
31      // Dequeue node from the front of the queue
32      const currentNode = queue.shift();
33    
34      // Push the value of current node to the level array
35      if(currentNode) {
36        levelValues.push(currentNode.val);
37
38        // Enqueue all children of the current node
39        queue.push(...currentNode.children);
40      }
41    }
42  
43    // Add the current level's values to the resultant array
44    result.push(levelValues);
45  }
46
47  // Return the resultant array after the tree has been traversed
48  return result;
49}
50
Not Sure What to Study? Take the 2-min Quiz:

Is the following code DFS or BFS?

1void search(Node root) {
2  if (!root) return;
3  visit(root);
4  root.visited = true;
5  for (Node node in root.adjacent) {
6    if (!node.visited) {
7      search(node);
8    }
9  }
10}

Time and Space Complexity

The given Python code defines a method to perform a level order traversal on an n-ary tree.

Time Complexity:

The time complexity of the code can be analyzed as follows:

  • We traverse each node exactly once.
  • The outer while loop runs as long as there are nodes to process which is exactly N times, where N is the total number of nodes.
  • The inner for loop runs for the number of nodes at the current level, collectively these will also run N times since every node is processed once.

Therefore, combining the complexity of both loops, the time complexity of the function is O(N) where N is the total number of nodes in the n-ary tree.

Space Complexity:

The space complexity of the code can be analyzed based on the additional space used by the data structures in the code:

  • The q (deque) stores the nodes of the tree at each level. In the worst case, it could store all the nodes of the tree, if they were on a single level. However, that would be a degenerate tree (more like a linked list). Typically, the largest amount of space would be used when the widest level of the tree is stored in the deque.
  • The t list accumulates nodes' values of the current level. In the worst-case scenario, this would equal the maximum breadth of the tree.

So, the space complexity is determined by the maximum number of nodes at any level (the maximum breadth of the tree), denoted as W. Therefore, the space complexity of the function is O(W).

Combining the above, the computational complexity of the code is:

  • Time Complexity: O(N)
  • Space Complexity: O(W)

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

Fast Track Your Learning with Our Quick Skills Quiz:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫