Facebook Pixel

589. N-ary Tree Preorder Traversal

Problem Description

This problem asks you to perform a preorder traversal on an n-ary tree (a tree where each node can have any number of children) and return a list containing the values of all nodes in preorder sequence.

In a preorder traversal, you visit nodes in the following order:

  1. Visit the current node (process its value)
  2. Recursively visit all children from left to right

For an n-ary tree, unlike a binary tree which has at most 2 children, each node can have multiple children stored in a list.

The solution uses a depth-first search (DFS) approach with recursion. The dfs helper function:

  • Checks if the current node is None and returns if so
  • Adds the current node's value to the answer list ans
  • Iterates through all children of the current node and recursively calls dfs on each child

The main function initializes an empty answer list, calls the DFS helper function starting from the root, and returns the populated answer list containing all node values in preorder sequence.

For example, if you have a tree where root has value 1, with children having values 3, 2, and 4, and the node with value 3 has children with values 5 and 6, the preorder traversal would visit nodes in this order: [1, 3, 5, 6, 2, 4].

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. In this case, we have a tree structure with a root node and each node can have multiple children.

Is it a tree?

  • Yes: The problem explicitly states we're working with an n-ary tree. A tree is a connected acyclic graph with a hierarchical structure, where we have one root node and each node (except the root) has exactly one parent.

DFS

  • Since we answered "yes" to the tree question, the flowchart leads us directly to DFS (Depth-First Search).

Conclusion: The flowchart suggests using DFS (Depth-First Search) for traversing the n-ary tree. This aligns perfectly with the preorder traversal requirement, as preorder is a type of DFS traversal where we:

  1. Process the current node first
  2. Then recursively visit all children from left to right

DFS is the natural choice for tree traversal problems, especially when we need to visit nodes in a specific order like preorder, inorder, or postorder. The recursive nature of DFS makes it straightforward to implement the preorder traversal pattern for an n-ary tree.

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

Intuition

When we need to traverse a tree and visit every node exactly once, we have two main strategies: breadth-first (level by level) or depth-first (going as deep as possible before backtracking). For preorder traversal, the requirement is to visit the parent node before any of its children, which naturally aligns with a depth-first approach.

The key insight is that preorder traversal follows a recursive pattern:

  1. Process the current node
  2. Apply the same logic to each child

This recursive nature makes DFS the perfect fit. Think of it like exploring a family tree - you'd first record a person's name, then explore their first child's entire lineage, then move to the second child's lineage, and so on. You go as deep as possible in one branch before moving to the next sibling branch.

For an n-ary tree, the only difference from a binary tree is that instead of having just left and right children, we have a list of children. The preorder principle remains the same: visit the root first, then recursively visit each child in order.

The recursive DFS approach emerges naturally because:

  • Each subtree is itself a valid n-ary tree with its own root
  • The traversal pattern for each subtree is identical to the main tree
  • We can apply the same function recursively to each child node

This self-similar structure of trees makes recursion the most intuitive solution. We handle the base case (null node), process the current node by adding its value to our result, then let recursion handle the complexity of traversing all descendants by simply calling the same function on each child.

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

Solution Approach

Following the recursive approach identified through our flowchart analysis, we implement DFS using a helper function. The solution uses a simple but effective pattern of combining recursion with a shared result list.

Data Structure Used:

  • A list ans to store the node values in preorder sequence

Implementation Steps:

  1. Initialize the result container: Create an empty list ans that will store all node values in preorder traversal order.

  2. Define the recursive DFS helper function dfs(root):

    • Base case: If the current node is None, simply return (this handles empty trees and leaf nodes' non-existent children)
    • Process current node: Append root.val to the answer list (this is the "pre" in preorder - we process the node before its children)
    • Recursive calls: Iterate through root.children and recursively call dfs(child) for each child
  3. Execute the traversal: Call dfs(root) from the main function to start the traversal from the root node.

  4. Return the result: Return the populated ans list containing all node values in preorder sequence.

Why this works:

  • The recursion naturally maintains the traversal order through the call stack
  • By appending the current node's value before making recursive calls, we ensure preorder sequence
  • The for loop over children ensures we visit children from left to right
  • The shared ans list accumulates results across all recursive calls

Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.

Space Complexity: O(h) for the recursion call stack, where h is the height of the tree. In the worst case (skewed tree), this could be O(n).

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Consider this n-ary tree:

        1
      / | \
     3  2  4
    / \
   5   6

Step-by-step execution:

  1. Initialize: Create empty list ans = []

  2. Start DFS from root (node 1):

    • Node is not None, so continue
    • Add 1 to ans: ans = [1]
    • Iterate through children: [3, 2, 4]
  3. First child - DFS on node 3:

    • Node is not None, so continue
    • Add 3 to ans: ans = [1, 3]
    • Iterate through children: [5, 6]
  4. First grandchild - DFS on node 5:

    • Node is not None, so continue
    • Add 5 to ans: ans = [1, 3, 5]
    • No children, return to node 3
  5. Second grandchild - DFS on node 6:

    • Node is not None, so continue
    • Add 6 to ans: ans = [1, 3, 6]
    • No children, return to node 3
    • All children of node 3 processed, return to node 1
  6. Second child - DFS on node 2:

    • Node is not None, so continue
    • Add 2 to ans: ans = [1, 3, 5, 6, 2]
    • No children, return to node 1
  7. Third child - DFS on node 4:

    • Node is not None, so continue
    • Add 4 to ans: ans = [1, 3, 5, 6, 2, 4]
    • No children, return to node 1
  8. Complete: All children of root processed, return ans = [1, 3, 5, 6, 2, 4]

The key pattern to observe: we always process the current node first (adding its value), then recursively explore each child from left to right. This ensures the preorder sequence where parent nodes appear before their children in the final result.

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 typing import List, Optional
10
11
12class Solution:
13    def preorder(self, root: Optional['Node']) -> List[int]:
14        """
15        Performs preorder traversal of an N-ary tree.
16      
17        Args:
18            root: The root node of the N-ary tree
19          
20        Returns:
21            List of node values in preorder sequence
22        """
23        def dfs(node: Optional['Node']) -> None:
24            """
25            Depth-first search helper function for preorder traversal.
26          
27            Args:
28                node: Current node being visited
29            """
30            # Base case: if node is None, return
31            if node is None:
32                return
33          
34            # Visit current node (preorder: process node before children)
35            result.append(node.val)
36          
37            # Recursively visit all children
38            for child in node.children:
39                dfs(child)
40      
41        # Initialize result list to store traversal output
42        result = []
43      
44        # Start DFS traversal from root
45        dfs(root)
46      
47        return result
48
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    // List to store the preorder traversal result
22    private List<Integer> result = new ArrayList<>();
23
24    /**
25     * Performs preorder traversal of an N-ary tree
26     * @param root The root node of the N-ary tree
27     * @return List containing node values in preorder sequence
28     */
29    public List<Integer> preorder(Node root) {
30        dfs(root);
31        return result;
32    }
33
34    /**
35     * Helper method to perform depth-first search for preorder traversal
36     * Preorder: visit current node first, then recursively visit all children
37     * @param node The current node being processed
38     */
39    private void dfs(Node node) {
40        // Base case: if node is null, return
41        if (node == null) {
42            return;
43        }
44      
45        // Process current node (preorder: root first)
46        result.add(node.val);
47      
48        // Recursively traverse all children from left to right
49        for (Node child : node.children) {
50            dfs(child);
51        }
52    }
53}
54
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     * Performs preorder traversal on an N-ary tree
25     * @param root - The root node of the N-ary tree
26     * @return A vector containing node values in preorder sequence
27     */
28    vector<int> preorder(Node* root) {
29        // Vector to store the traversal result
30        vector<int> result;
31      
32        // Define recursive DFS lambda function for preorder traversal
33        function<void(Node*)> dfs = [&](Node* node) {
34            // Base case: if node is null, return
35            if (!node) {
36                return;
37            }
38          
39            // Process current node (preorder: root first)
40            result.push_back(node->val);
41          
42            // Recursively traverse all children from left to right
43            for (Node* child : node->children) {
44                dfs(child);
45            }
46        };
47      
48        // Start the traversal from root
49        dfs(root);
50      
51        return result;
52    }
53};
54
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 preorder traversal on an N-ary tree
15 * @param root - The root node of the N-ary tree
16 * @returns An array containing node values in preorder sequence
17 */
18function preorder(root: Node | null): number[] {
19    // Initialize result array to store traversal values
20    const result: number[] = [];
21  
22    /**
23     * Helper function to perform depth-first search traversal
24     * @param node - Current node being processed
25     */
26    const depthFirstSearch = (node: Node | null): void => {
27        // Base case: if node is null, return early
28        if (!node) {
29            return;
30        }
31      
32        // Process current node (preorder: root first)
33        result.push(node.val);
34      
35        // Recursively traverse all children from left to right
36        for (const child of node.children) {
37            depthFirstSearch(child);
38        }
39    };
40  
41    // Start traversal from root
42    depthFirstSearch(root);
43  
44    return result;
45}
46

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 during the traversal. For each node visited, the algorithm performs a constant amount of work - appending the node's value to the result list and iterating through its children. Since we visit all n nodes exactly once, the time complexity is O(n).

Space Complexity: O(n)

The space complexity has two components:

  1. Recursion call stack: In the worst case, the tree could be skewed (like a linked list), causing the recursion depth to reach n. This would require O(n) space for the call stack.
  2. Output array: The ans list stores the values of all n nodes, requiring O(n) space.

Even in the best case (a balanced tree), the recursion depth would be O(log n), but the output array still requires O(n) space. Therefore, the overall space complexity is O(n).

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

Common Pitfalls

1. Not Handling None Children in the Children List

A common mistake is assuming that the children attribute is always a valid list. In some implementations, children might be None instead of an empty list for leaf nodes, causing the iteration for child in node.children: to fail with a TypeError.

Problematic Code:

def dfs(node):
    if node is None:
        return
    result.append(node.val)
    for child in node.children:  # This fails if node.children is None
        dfs(child)

Solution:

def dfs(node):
    if node is None:
        return
    result.append(node.val)
    # Check if children exists and is not None
    if node.children:
        for child in node.children:
            dfs(child)

2. Using a Mutable Default Argument

Some developers try to avoid using a nested function by passing the result list as a parameter with a default value. This creates a subtle bug where the default list persists across multiple function calls.

Problematic Code:

def preorder(self, root, result=[]):  # Dangerous: mutable default argument
    if root is None:
        return result
    result.append(root.val)
    for child in root.children:
        self.preorder(child, result)
    return result

Solution: Use None as the default and create a new list when needed:

def preorder(self, root, result=None):
    if result is None:
        result = []
    if root is None:
        return result
    result.append(root.val)
    for child in root.children:
        self.preorder(child, result)
    return result

3. Incorrect Variable Scope in Nested Function

When using a nested function approach, forgetting to declare the result list in the outer function scope or trying to reassign it (rather than mutate it) in the inner function can cause issues.

Problematic Code:

def preorder(self, root):
    def dfs(node):
        if node is None:
            return
        result = []  # Wrong: creates new local variable each time
        result.append(node.val)
        for child in node.children:
            dfs(child)
  
    dfs(root)
    return result  # NameError: result is not defined here

Solution: Declare the result list in the outer scope and mutate it (don't reassign):

def preorder(self, root):
    result = []  # Declare in outer scope
  
    def dfs(node):
        if node is None:
            return
        result.append(node.val)  # Mutate, don't reassign
        for child in node.children:
            dfs(child)
  
    dfs(root)
    return result
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

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

Load More