Facebook Pixel

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:

  1. First, recursively visit all children of a node (from left to right)
  2. 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:

  1. Checks if the current node is None (base case)
  2. Recursively calls itself for each child of the current node
  3. 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:

  1. Postorder traversal is inherently a depth-first operation - we need to go as deep as possible to the leaf nodes before processing parent nodes
  2. DFS naturally handles the recursive nature of visiting all children before processing the current node
  3. 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.

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

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:

  1. The base case handles None nodes (empty trees)
  2. The recursive case processes all children first (the loop through root.children)
  3. 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:

  1. Base Case: If root is None, we simply return. This handles empty trees and serves as the recursion termination condition.

  2. 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.

  3. 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

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:

  1. It first processes all descendants through recursive calls
  2. Each recursive call follows the same pattern, ensuring all nodes in a subtree are processed before the subtree's root
  3. 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) calls dfs(3), then dfs(2), then dfs(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 Evaluator

Example 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:

  1. Start: Call dfs(1), ans = []

    • Node 1 has children [3, 2, 4]
    • Must process all children before adding 1
  2. Process first child: Call dfs(3)

    • Node 3 has children [5, 6]
    • Must process children before adding 3
  3. Process node 5: Call dfs(5)

    • Node 5 has no children
    • Add 5 to ans → ans = [5]
    • Return to dfs(3)
  4. Process node 6: Call dfs(6)

    • Node 6 has no children
    • Add 6 to ans → ans = [5, 6]
    • Return to dfs(3)
  5. Complete node 3: All children of 3 are processed

    • Add 3 to ans → ans = [5, 6, 3]
    • Return to dfs(1)
  6. Process second child: Call dfs(2)

    • Node 2 has no children
    • Add 2 to ans → ans = [5, 6, 3, 2]
    • Return to dfs(1)
  7. 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)
  8. 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:

  1. 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 be O(log n). However, for the worst-case analysis, we consider O(n) for the call stack.

  2. Output array (ans): The result list stores exactly n values (one for each node), requiring O(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.

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

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More