Facebook Pixel

145. Binary Tree Postorder Traversal

Problem Description

Given the root of a binary tree, you need to return the postorder traversal of its nodes' values.

In postorder traversal, you visit the nodes in the following order:

  1. First, traverse the left subtree
  2. Then, traverse the right subtree
  3. Finally, visit the root node

This means for each node, you process all of its descendants before processing the node itself. The traversal follows a "left-right-root" pattern.

For example, if you have a binary tree:

    1
   / \
  2   3

The postorder traversal would visit the nodes in this sequence: [2, 3, 1]

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

  • Returns immediately if the current node is None (base case)
  • Recursively calls itself on the left child
  • Recursively calls itself on the right child
  • Appends the current node's value to the result list

The recursive calls naturally handle the "left-right-root" ordering, as the function processes both children completely before adding the current node's value to the answer list. The main function initializes an empty result list ans, calls the DFS helper on the root, and returns the populated list.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree structure, which is a hierarchical data structure with a root node and child nodes.

DFS

  • Yes: Since we identified this as a tree problem, the flowchart leads us directly to DFS (Depth-First Search).

Conclusion: The flowchart suggests using DFS for this binary tree traversal problem. This makes perfect sense because:

  1. Tree traversal problems are naturally suited for DFS as we need to explore nodes deeply before backtracking
  2. Postorder traversal specifically requires visiting children before the parent node, which aligns with DFS's recursive nature
  3. DFS allows us to process the left subtree completely, then the right subtree completely, before processing the current node - exactly what postorder traversal requires

The DFS pattern is implemented through recursion in the solution, where the recursive calls handle the depth-first exploration of the tree structure automatically.

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

Intuition

When we think about postorder traversal, we need to visit nodes in a specific order: left subtree, right subtree, then the current node. This "bottom-up" pattern naturally suggests recursion - we need to go deep into the tree first before processing higher-level nodes.

The key insight is that recursion inherently provides us with a call stack that remembers where we've been. When we make a recursive call to traverse the left subtree, the function automatically waits for that entire subtree to be processed before moving to the next line of code. This built-in waiting mechanism is exactly what we need for postorder traversal.

Consider what happens at each node:

  1. We can't process the current node's value yet - we need to visit its children first
  2. We recursively explore the left child and all its descendants
  3. We recursively explore the right child and all its descendants
  4. Only after both recursive calls return can we process the current node

This matches the recursive function structure perfectly:

dfs(root.left)   # Process entire left subtree
dfs(root.right)  # Process entire right subtree
ans.append(root.val)  # Finally, process current node

The base case if root is None: return handles leaf nodes' children and prevents infinite recursion. The beauty of this approach is that the recursive call stack naturally maintains the correct ordering - we don't need to manually track which nodes to visit next or maintain our own stack data structure. The function call stack does this work for us, making the code clean and intuitive.

Solution Approach

The solution implements a recursive DFS approach to achieve postorder traversal. Let's walk through the implementation step by step:

Main Function Structure: The solution creates a helper function dfs inside the main postorderTraversal method. This nested function design allows us to access the ans list without passing it as a parameter in every recursive call.

Recursive DFS Implementation: The dfs function follows this pattern:

  1. Base Case: if root is None: return - When we reach a null node (beyond a leaf), we simply return without doing anything
  2. Left Subtree Traversal: dfs(root.left) - Recursively process the entire left subtree
  3. Right Subtree Traversal: dfs(root.right) - Recursively process the entire right subtree
  4. Process Current Node: ans.append(root.val) - Add the current node's value to the result list

Data Structure Used:

  • A simple list ans = [] to collect the node values in postorder sequence
  • The implicit call stack maintained by Python during recursion

Execution Flow Example: For a tree like:

      1
     / \
    2   3

The execution proceeds as:

  1. dfs(1) is called
  2. Inside dfs(1), we call dfs(2) for the left child
  3. Inside dfs(2), we call dfs(None) for left child - returns immediately
  4. Inside dfs(2), we call dfs(None) for right child - returns immediately
  5. Inside dfs(2), we append 2 to ans
  6. Back in dfs(1), we call dfs(3) for the right child
  7. Inside dfs(3), both child calls return immediately (null nodes)
  8. Inside dfs(3), we append 3 to ans
  9. Back in dfs(1), we append 1 to ans
  10. Final result: [2, 3, 1]

The recursive nature ensures that for any node, all its descendants are processed before the node itself, which is exactly what postorder traversal requires.

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 trace through the postorder traversal algorithm with a concrete example:

Consider this binary tree:

      4
     / \
    2   5
   / \
  1   3

We want to achieve postorder traversal, which should give us: [1, 3, 2, 5, 4]

Step-by-step execution:

  1. Start with root (4):

    • Call dfs(4)
    • Before processing 4, we must traverse its left subtree
  2. Traverse left subtree of 4 (node 2):

    • Call dfs(2)
    • Before processing 2, traverse its left subtree
  3. Traverse left subtree of 2 (node 1):

    • Call dfs(1)
    • Call dfs(None) for 1's left child → returns immediately
    • Call dfs(None) for 1's right child → returns immediately
    • Append 1 to ansans = [1]
    • Return from dfs(1)
  4. Back in dfs(2), traverse right subtree (node 3):

    • Call dfs(3)
    • Call dfs(None) for 3's left child → returns immediately
    • Call dfs(None) for 3's right child → returns immediately
    • Append 3 to ansans = [1, 3]
    • Return from dfs(3)
  5. Still in dfs(2), process node 2:

    • Both subtrees are complete
    • Append 2 to ansans = [1, 3, 2]
    • Return from dfs(2)
  6. Back in dfs(4), traverse right subtree (node 5):

    • Call dfs(5)
    • Call dfs(None) for 5's left child → returns immediately
    • Call dfs(None) for 5's right child → returns immediately
    • Append 5 to ansans = [1, 3, 2, 5]
    • Return from dfs(5)
  7. Finally in dfs(4), process root node 4:

    • Both subtrees are complete
    • Append 4 to ansans = [1, 3, 2, 5, 4]
    • Return from dfs(4)

Key Observations:

  • The recursion naturally creates a "bottom-up" processing order
  • Each node waits for both its children to be fully processed
  • The deepest nodes (leaves) are processed first
  • The root is always processed last
  • The call stack manages the traversal order automatically - we don't need any additional data structures to track which nodes to visit next

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8class Solution:
9    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
10        """
11        Perform postorder traversal of a binary tree.
12      
13        Args:
14            root: The root node of the binary tree
15          
16        Returns:
17            A list containing the values of nodes in postorder sequence
18        """
19      
20        def traverse_postorder(node: Optional[TreeNode]) -> None:
21            """
22            Helper function to recursively traverse the tree in postorder.
23            Postorder: left subtree -> right subtree -> root
24          
25            Args:
26                node: Current node being processed
27            """
28            # Base case: if node is None, return
29            if node is None:
30                return
31          
32            # First, recursively traverse the left subtree
33            traverse_postorder(node.left)
34          
35            # Then, recursively traverse the right subtree
36            traverse_postorder(node.right)
37          
38            # Finally, process the current node by adding its value to result
39            result.append(node.val)
40      
41        # Initialize the result list to store traversal values
42        result: List[int] = []
43      
44        # Start the postorder traversal from the root
45        traverse_postorder(root)
46      
47        return result
48
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    // List to store the result of postorder traversal
18    private List<Integer> result = new ArrayList<>();
19
20    /**
21     * Performs postorder traversal of a binary tree
22     * @param root The root node of the binary tree
23     * @return List of node values in postorder sequence (left, right, root)
24     */
25    public List<Integer> postorderTraversal(TreeNode root) {
26        performPostorderDFS(root);
27        return result;
28    }
29
30    /**
31     * Helper method to recursively traverse the tree in postorder
32     * @param node The current node being processed
33     */
34    private void performPostorderDFS(TreeNode node) {
35        // Base case: if node is null, return
36        if (node == null) {
37            return;
38        }
39      
40        // Traverse left subtree first
41        performPostorderDFS(node.left);
42      
43        // Traverse right subtree second
44        performPostorderDFS(node.right);
45      
46        // Process current node last (postorder)
47        result.add(node.val);
48    }
49}
50
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    /**
15     * Performs postorder traversal of a binary tree
16     * @param root - The root node of the binary tree
17     * @return A vector containing node values in postorder sequence (left, right, root)
18     */
19    vector<int> postorderTraversal(TreeNode* root) {
20        vector<int> result;  // Store the traversal result
21      
22        // Define recursive function for depth-first search
23        function<void(TreeNode*)> dfs = [&](TreeNode* node) {
24            // Base case: if node is null, return
25            if (!node) {
26                return;
27            }
28          
29            // Postorder traversal: left -> right -> root
30            dfs(node->left);   // Traverse left subtree
31            dfs(node->right);  // Traverse right subtree
32            result.push_back(node->val);  // Process current node
33        };
34      
35        // Start the traversal from root
36        dfs(root);
37      
38        return result;
39    }
40};
41
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Performs postorder traversal of a binary tree
17 * @param root - The root node of the binary tree
18 * @returns An array containing node values in postorder sequence (left, right, root)
19 */
20function postorderTraversal(root: TreeNode | null): number[] {
21    // Array to store the traversal result
22    const result: number[] = [];
23  
24    /**
25     * Helper function to perform depth-first search in postorder
26     * @param node - The current node being processed
27     */
28    const performDFS = (node: TreeNode | null): void => {
29        // Base case: if node is null, return
30        if (!node) {
31            return;
32        }
33      
34        // Traverse left subtree first
35        performDFS(node.left);
36      
37        // Then traverse right subtree
38        performDFS(node.right);
39      
40        // Finally, process the current node
41        result.push(node.val);
42    };
43  
44    // Start the traversal from the root
45    performDFS(root);
46  
47    return result;
48}
49

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node in the tree is visited exactly once during the traversal. For each node, the operations performed (visiting left child, visiting right child, and appending the value to the result list) take constant time O(1). Since we visit all n nodes exactly once, the total time complexity is O(n), where n is the number of nodes in the binary tree.

Space Complexity: O(n)

The space complexity consists of two components:

  1. Recursive call stack: In the worst case, the tree could be completely unbalanced (e.g., a skewed tree where each node has only one child), resulting in a recursion depth of O(n). In the best case of a balanced tree, the recursion depth would be O(log n). However, we consider the worst case, giving us O(n) space for the call stack.

  2. Output array: The ans list stores the values of all n nodes in the tree, requiring O(n) space.

Therefore, the overall space complexity is O(n), dominated by both the recursive call stack in the worst case and the output array that stores all node values.

Common Pitfalls

1. Modifying the Tree Structure During Traversal

A common mistake is accidentally modifying the tree structure while traversing, such as setting nodes to None or changing node connections. This can happen when trying to "mark" visited nodes.

Incorrect approach:

def postorderTraversal(self, root):
    result = []
    def dfs(node):
        if node is None:
            return
        dfs(node.left)
        dfs(node.right)
        result.append(node.val)
        node = None  # Wrong! This doesn't affect the tree, just the local variable
    dfs(root)
    return result

Solution: Never modify the tree nodes during traversal. The recursive call stack naturally handles the visiting order.

2. Confusing Traversal Orders

Developers often mix up the order of operations between preorder (root-left-right), inorder (left-root-right), and postorder (left-right-root).

Incorrect postorder (actually preorder):

def postorderTraversal(self, root):
    result = []
    def dfs(node):
        if node is None:
            return
        result.append(node.val)  # Wrong position for postorder!
        dfs(node.left)
        dfs(node.right)
    dfs(root)
    return result

Solution: Remember the mnemonic - in postorder, the node is processed "post" (after) its children. Always: left subtree → right subtree → current node.

3. Stack Overflow with Deep Trees

Python has a default recursion limit (usually 1000). For extremely deep or unbalanced trees, the recursive solution can cause a stack overflow.

Solution: Use an iterative approach with an explicit stack for very deep trees:

def postorderTraversal(self, root):
    if not root:
        return []
  
    result = []
    stack = [(root, False)]
  
    while stack:
        node, visited = stack.pop()
        if visited:
            result.append(node.val)
        else:
            stack.append((node, True))
            if node.right:
                stack.append((node.right, False))
            if node.left:
                stack.append((node.left, False))
  
    return result

4. Forgetting to Handle Empty Tree

While the recursive solution naturally handles None root, forgetting this case can cause issues in modified implementations.

Problematic code:

def postorderTraversal(self, root):
    result = []
    def dfs(node):
        # Missing base case check!
        dfs(node.left)  # Will crash if root is None
        dfs(node.right)
        result.append(node.val)
    dfs(root)
    return result

Solution: Always check for None at the beginning of the recursive function or before calling it.

5. Using Global Variables Instead of Enclosure

Using global variables or class attributes instead of proper scoping can cause issues in concurrent scenarios or when the function is called multiple times.

Problematic approach:

class Solution:
    def __init__(self):
        self.result = []  # Class attribute - persists between calls!
  
    def postorderTraversal(self, root):
        def dfs(node):
            if node is None:
                return
            dfs(node.left)
            dfs(node.right)
            self.result.append(node.val)
        dfs(root)
        return self.result  # Will accumulate values from previous calls

Solution: Always initialize the result list inside the function to ensure a fresh start for each call.

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

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More