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:
- First, traverse the left subtree
- Then, traverse the right subtree
- 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:
- Tree traversal problems are naturally suited for DFS as we need to explore nodes deeply before backtracking
- Postorder traversal specifically requires visiting children before the parent node, which aligns with DFS's recursive nature
- 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.
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:
- We can't process the current node's value yet - we need to visit its children first
- We recursively explore the left child and all its descendants
- We recursively explore the right child and all its descendants
- 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:
- Base Case:
if root is None: return
- When we reach a null node (beyond a leaf), we simply return without doing anything - Left Subtree Traversal:
dfs(root.left)
- Recursively process the entire left subtree - Right Subtree Traversal:
dfs(root.right)
- Recursively process the entire right subtree - 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:
dfs(1)
is called- Inside
dfs(1)
, we calldfs(2)
for the left child - Inside
dfs(2)
, we calldfs(None)
for left child - returns immediately - Inside
dfs(2)
, we calldfs(None)
for right child - returns immediately - Inside
dfs(2)
, we append2
toans
- Back in
dfs(1)
, we calldfs(3)
for the right child - Inside
dfs(3)
, both child calls return immediately (null nodes) - Inside
dfs(3)
, we append3
toans
- Back in
dfs(1)
, we append1
toans
- 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 EvaluatorExample 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:
-
Start with root (4):
- Call
dfs(4)
- Before processing 4, we must traverse its left subtree
- Call
-
Traverse left subtree of 4 (node 2):
- Call
dfs(2)
- Before processing 2, traverse its left subtree
- Call
-
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
ans
→ans = [1]
- Return from
dfs(1)
- Call
-
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
ans
→ans = [1, 3]
- Return from
dfs(3)
- Call
-
Still in dfs(2), process node 2:
- Both subtrees are complete
- Append 2 to
ans
→ans = [1, 3, 2]
- Return from
dfs(2)
-
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
ans
→ans = [1, 3, 2, 5]
- Return from
dfs(5)
- Call
-
Finally in dfs(4), process root node 4:
- Both subtrees are complete
- Append 4 to
ans
→ans = [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:
-
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 beO(log n)
. However, we consider the worst case, giving usO(n)
space for the call stack. -
Output array: The
ans
list stores the values of alln
nodes in the tree, requiringO(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.
Which of the following problems can be solved with backtracking (select multiple)
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!