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:
- Visit the current node (process its value)
- 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:
- Process the current node first
- 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.
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:
- Process the current node
- 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:
-
Initialize the result container: Create an empty list
ans
that will store all node values in preorder traversal order. -
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 calldfs(child)
for each child
- Base case: If the current node is
-
Execute the traversal: Call
dfs(root)
from the main function to start the traversal from the root node. -
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 EvaluatorExample 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:
-
Initialize: Create empty list
ans = []
-
Start DFS from root (node 1):
- Node is not None, so continue
- Add 1 to ans:
ans = [1]
- Iterate through children: [3, 2, 4]
-
First child - DFS on node 3:
- Node is not None, so continue
- Add 3 to ans:
ans = [1, 3]
- Iterate through children: [5, 6]
-
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
-
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
-
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
-
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
-
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:
- 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 requireO(n)
space for the call stack. - Output array: The
ans
list stores the values of alln
nodes, requiringO(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
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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 dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Want a Structured Path to Master System Design Too? Don’t Miss This!