590. N-ary Tree Postorder Traversal
Problem Description
You are given the root of an n-ary tree (a tree where each node can have any number of children, not just two like in a binary tree). Your task is to return the postorder traversal of the tree's node values.
In a postorder traversal, you visit nodes in the following order:
- First, recursively visit all children of a node (from left to right)
- Then, visit the node itself
For example, if you have a tree where node 1 has children [3, 2, 4], and node 3 has children [5, 6]:
- The postorder traversal would visit nodes in this order: 5, 6, 3, 2, 4, 1
- This is because we visit all descendants of a node before visiting the node itself
The solution uses a recursive depth-first search (DFS) approach. The dfs
function:
- Checks if the current node is
None
(base case) - Recursively calls itself for each child of the current node
- After all children have been processed, appends the current node's value to the result list
The recursion naturally handles the postorder property - by processing all children before adding the current node's value, we ensure that all descendants appear before their ancestors in the final result.
The time complexity is O(n)
where n is the number of nodes, as we visit each node exactly once. The space complexity is O(h)
where h is the height of the tree, due to the recursion call stack.
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, where we have a root node and each node can have multiple children.
DFS
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart directs us to use Depth-First Search (DFS).
Conclusion: The flowchart suggests using DFS for this n-ary tree traversal problem. This makes perfect sense because:
- Postorder traversal is inherently a depth-first operation - we need to go as deep as possible to the leaf nodes before processing parent nodes
- DFS naturally handles the recursive nature of visiting all children before processing the current node
- The tree structure guarantees no cycles, making DFS safe and efficient without needing to track visited nodes
The DFS approach perfectly aligns with the postorder requirement: recursively visit all children first (going deep), then process the current node's value on the way back up the recursion stack.
Intuition
When we think about postorder traversal, the key insight is that we need to visit all children before visiting the parent. This naturally suggests a recursive approach where we "dive deep" into the tree first.
Consider how we would manually perform a postorder traversal: we'd start at the root, but we can't record its value yet. Instead, we need to explore all its subtrees first. For each child, we face the same situation - we can't record that child's value until we've explored all of its children. This pattern continues until we reach leaf nodes, which have no children and can be recorded immediately.
This recursive pattern maps perfectly to DFS. The call stack naturally handles the "remember where we came from" aspect - when we finish exploring a subtree, the recursion automatically returns us to the parent node. At that point, all children have been processed, so we can finally record the parent's value.
The beauty of this approach is its simplicity. The recursive structure mirrors the tree structure itself:
- The base case handles
None
nodes (empty trees) - The recursive case processes all children first (the loop through
root.children
) - Only after all recursive calls return do we record the current node's value
This order is guaranteed by the function call stack - a parent function can't complete until all its child function calls complete. This inherent property of recursion gives us the postorder traversal "for free" without explicitly managing any traversal state.
Learn more about Stack, Tree and Depth-First Search patterns.
Solution Approach
The solution implements a recursive DFS traversal to achieve postorder ordering. Let's walk through the implementation step by step:
Main Function Structure:
The postorder
function serves as the entry point, initializing an empty list ans
to store the result and calling the helper function dfs
.
Recursive Helper Function:
The dfs
function implements the core traversal logic:
-
Base Case: If
root is None
, we simply return. This handles empty trees and serves as the recursion termination condition. -
Recursive Case: For a non-null node, we iterate through all its children using
for child in root.children
. This works because n-ary tree nodes store their children in a list. -
Postorder Property: The key to achieving postorder is the sequence of operations:
- First, recursively call
dfs(child)
for each child - Only after all recursive calls complete, append
root.val
to the answer list
- First, recursively call
Data Structure Used:
- A simple list
ans
accumulates the node values in postorder sequence - The function call stack implicitly maintains the traversal state
Why This Works:
When dfs(root)
is called on any node:
- It first processes all descendants through recursive calls
- Each recursive call follows the same pattern, ensuring all nodes in a subtree are processed before the subtree's root
- The node's value is added to
ans
only after all its subtrees are completely processed
For example, with a tree where node 1 has children [3, 2, 4]:
dfs(1)
callsdfs(3)
, thendfs(2)
, thendfs(4)
- Each child's entire subtree is processed before moving to the next child
- Finally, after all children are done, 1 is added to the result
The time complexity is O(n)
as we visit each node exactly once, and the space complexity is O(h)
for the recursion stack, where h
is the height of the tree.
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 recursive DFS solution works for postorder traversal.
Consider this n-ary tree:
1 / | \ 3 2 4 / \ 5 6
We want to achieve postorder traversal: [5, 6, 3, 2, 4, 1]
Step-by-step execution:
-
Start: Call
dfs(1)
, ans = []- Node 1 has children [3, 2, 4]
- Must process all children before adding 1
-
Process first child: Call
dfs(3)
- Node 3 has children [5, 6]
- Must process children before adding 3
-
Process node 5: Call
dfs(5)
- Node 5 has no children
- Add 5 to ans → ans = [5]
- Return to dfs(3)
-
Process node 6: Call
dfs(6)
- Node 6 has no children
- Add 6 to ans → ans = [5, 6]
- Return to dfs(3)
-
Complete node 3: All children of 3 are processed
- Add 3 to ans → ans = [5, 6, 3]
- Return to dfs(1)
-
Process second child: Call
dfs(2)
- Node 2 has no children
- Add 2 to ans → ans = [5, 6, 3, 2]
- Return to dfs(1)
-
Process third child: Call
dfs(4)
- Node 4 has no children
- Add 4 to ans → ans = [5, 6, 3, 2, 4]
- Return to dfs(1)
-
Complete root: All children of 1 are processed
- Add 1 to ans → ans = [5, 6, 3, 2, 4, 1]
- Return final result
The key insight: At each node, we completely finish processing all children (and their descendants) before adding the current node's value. The recursion stack ensures we return to parent nodes in the correct order, giving us the postorder traversal naturally.
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 postorder(self, root: Optional['Node']) -> List[int]:
14 """
15 Performs post-order traversal of an N-ary tree.
16
17 Post-order traversal visits nodes in the following order:
18 1. Recursively visit all children from left to right
19 2. Visit the current node
20
21 Args:
22 root: The root node of the N-ary tree
23
24 Returns:
25 A list of node values in post-order traversal sequence
26 """
27 def dfs(node: Optional['Node']) -> None:
28 """
29 Depth-first search helper function for post-order traversal.
30
31 Args:
32 node: Current node being visited
33 """
34 # Base case: if node is None, return
35 if node is None:
36 return
37
38 # Recursively traverse all children
39 for child in node.children:
40 dfs(child)
41
42 # After visiting all children, add current node's value
43 result.append(node.val)
44
45 # Initialize result list to store traversal values
46 result: List[int] = []
47
48 # Start DFS traversal from root
49 dfs(root)
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 // List to store the result of postorder traversal
22 private List<Integer> result = new ArrayList<>();
23
24 /**
25 * Performs postorder traversal on an N-ary tree
26 * @param root The root node of the N-ary tree
27 * @return List of node values in postorder sequence
28 */
29 public List<Integer> postorder(Node root) {
30 performPostorderTraversal(root);
31 return result;
32 }
33
34 /**
35 * Helper method to recursively traverse the tree in postorder
36 * Postorder: visit all children first, then visit the current node
37 * @param node The current node being processed
38 */
39 private void performPostorderTraversal(Node node) {
40 // Base case: if node is null, return
41 if (node == null) {
42 return;
43 }
44
45 // Recursively visit all children nodes first
46 for (Node child : node.children) {
47 performPostorderTraversal(child);
48 }
49
50 // After visiting all children, add current node's value to result
51 result.add(node.val);
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 post-order traversal on an N-ary tree
25 * Post-order: visit all children first, then visit the current node
26 * @param root - The root node of the N-ary tree
27 * @return A vector containing node values in post-order sequence
28 */
29 vector<int> postorder(Node* root) {
30 vector<int> result;
31
32 // Define recursive DFS lambda function for post-order traversal
33 function<void(Node*)> dfs = [&](Node* node) {
34 // Base case: if node is null, return immediately
35 if (!node) {
36 return;
37 }
38
39 // Recursively traverse all children first (post-order)
40 for (Node* child : node->children) {
41 dfs(child);
42 }
43
44 // After visiting all children, add current node's value
45 result.push_back(node->val);
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 post-order 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 post-order sequence
17 */
18function postorder(root: Node | null): number[] {
19 // Initialize result array to store traversal values
20 const result: number[] = [];
21
22 /**
23 * Depth-first search helper function for post-order traversal
24 * @param node - Current node being processed
25 */
26 const performDFS = (node: Node | null): void => {
27 // Base case: if node is null, return immediately
28 if (!node) {
29 return;
30 }
31
32 // Recursively visit all children first (left to right)
33 for (const childNode of node.children) {
34 performDFS(childNode);
35 }
36
37 // Process current node after all children have been visited
38 result.push(node.val);
39 };
40
41 // Start DFS traversal from root
42 performDFS(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 in postorder fashion. Each node in the tree is visited exactly once during the traversal. At each node, the algorithm:
- Iterates through all its children (constant time per child reference)
- Appends the node's value to the result list (amortized
O(1)
operation)
Since we visit all n
nodes exactly once and perform constant-time operations at each node, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of two components:
-
Recursion call stack: In the worst case (a skewed tree where each node has only one child), the recursion depth can be
O(n)
. In the best case (a balanced tree), the recursion depth would beO(log n)
. However, for the worst-case analysis, we considerO(n)
for the call stack. -
Output array (
ans
): The result list stores exactlyn
values (one for each node), requiringO(n)
space.
The dominant factor is O(n)
, so the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting to Access children
on None Nodes
A common mistake is checking for None
after trying to access the node's properties:
Incorrect Implementation:
def dfs(node):
for child in node.children: # Error if node is None!
dfs(child)
if node is not None:
result.append(node.val)
This will raise an AttributeError
when node
is None
because we're trying to access node.children
before checking if the node exists.
Correct Implementation:
def dfs(node):
if node is None: # Check for None FIRST
return
for child in node.children:
dfs(child)
result.append(node.val)
2. Handling Empty or None Children Lists
Some implementations of n-ary trees might have node.children = None
instead of an empty list []
for leaf nodes:
Potential Issue:
def dfs(node):
if node is None:
return
for child in node.children: # TypeError if children is None!
dfs(child)
result.append(node.val)
Robust Solution:
def dfs(node):
if node is None:
return
if node.children: # Check if children exists and is not empty
for child in node.children:
dfs(child)
result.append(node.val)
3. Using a Global Variable Instead of Closure
Using a global variable for the result list can cause issues in environments where multiple test cases run:
Problematic Approach:
class Solution:
def __init__(self):
self.result = [] # Instance variable persists between calls
def postorder(self, root):
self.dfs(root)
return self.result # May contain values from previous calls!
def dfs(self, node):
if node is None:
return
for child in node.children:
self.dfs(child)
self.result.append(node.val)
Better Approach: Use a local variable within the function scope, as shown in the original solution, or clear the result list at the beginning of each call.
4. Forgetting to Return the Result
A simple but common mistake is forgetting to return the accumulated result:
Incorrect:
def postorder(self, root):
result = []
def dfs(node):
if node is None:
return
for child in node.children:
dfs(child)
result.append(node.val)
dfs(root)
# Forgot to return result!
This function would return None
instead of the expected list of values.
5. Modifying the Tree During Traversal
Attempting to modify the tree structure or node values during traversal can lead to unexpected behavior:
Problematic:
def dfs(node):
if node is None:
return
for child in node.children:
dfs(child)
result.append(node.val)
node.children = [] # Don't modify the tree!
The traversal should be read-only to maintain the tree's integrity for potential subsequent operations.
Which algorithm should you use to find a node that is close to the root of the tree?
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!