Loading problems...
TL;DR: The optimal solution recursively visits the left child, then the right child, and finally processes the current node, accumulating values into a single result list.
The LeetCode 145 problem, Binary Tree Postorder Traversal, asks us to traverse a binary tree and return a list of its node values in a specific order. In a postorder traversal, we must process the subtrees of a node before processing the node itself. Specifically, for every node, the traversal order is:
This is a fundamental tree operation and a popular interview question used to test understanding of Depth-First Search (DFS) and recursion.
A naive approach to solving the Binary Tree Postorder Traversal problem often stems from a direct translation of the mathematical definition into code using functional list concatenation.
In this approach, a developer might implement a function that returns a new list for every node. The logic typically looks like this:
postorder(node) = postorder(node.left) + postorder(node.right) + [node.val]
function postorder(node):
if node is null:
return empty list
left_list = postorder(node.left)
right_list = postorder(node.right)
// Concatenate lists
return concatenate(left_list, right_list, [node.val])While this approach is logically correct, it is inefficient for large trees. The excessive memory allocation and data copying can lead to poor performance or Memory Limit Exceeded (MLE) errors in strict environments, and it fails to demonstrate the optimal manipulation required in technical interviews.
The key intuition for an optimal LeetCode 145 Solution is to use a single mutable data structure (like a dynamic array or vector) that is passed by reference (or available globally) throughout the recursion.
Instead of creating and returning new lists at each node, we maintain one result list. We traverse the tree using the standard recursive stack. The crucial invariant of Postorder Traversal is that a node is never added to the result list until both its left and right subtrees have fully completed their execution.
Visual Description:
Imagine the recursion tree. The algorithm plunges deep down the left side of the tree first. Nothing is written to the result list during this descent. It is only when the recursion hits a null leaf and returns (pops from the stack) that the algorithm considers adding a value. Even then, it checks the right child. Only after the function calls for the left and right children return does the current node "commit" its value to the list. This ensures the "bottom-up" processing characteristic of Postorder traversal.

To implement the optimal solution for interviews, we utilize a helper function (or a private method) to handle the recursion.
result to store the traversal sequence.traverse function that takes a node and the result structure.node is null, we return immediately. This stops the recursion at leaf nodes.traverse on node.left. This ensures all nodes in the left subtree are processed and added to result before the current node.traverse on node.right. This ensures all nodes in the right subtree are processed and added to result before the current node.node.val to result.This strategy guarantees that for any node , all nodes in its subtrees appear earlier in the result list than itself.
result.dfs(root, result).node):
node null? If yes, return to caller.node.left. The program stack grows. This continues until a leaf's left child (null) is reached.node.right.result.add(node.val).node and pops off the stack, returning control to node's parent.dfs(root) returns, result contains the complete postorder traversal.The correctness follows directly from the inductive definition of Postorder Traversal.
cpp/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: vector<int> postorderTraversal(TreeNode* root) { vector<int> result; traverse(root, result); return result; } private: // Helper function implementing the Recursive Postorder pattern void traverse(TreeNode* node, vector<int>& result) { if (node == nullptr) { return; } // Pattern: Left -> Right -> Root traverse(node->left, result); traverse(node->right, result); result.push_back(node->val); } };
java/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public List<Integer> postorderTraversal(TreeNode root) { List<Integer> result = new ArrayList<>(); traverse(root, result); return result; } // Helper method strictly following the DFS Postorder logic private void traverse(TreeNode node, List<Integer> result) { if (node == null) { return; } // Visit Left Subtree traverse(node.left, result); // Visit Right Subtree traverse(node.right, result); // Visit Root Node result.add(node.val); } }
python# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution: def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: result = [] def traverse(node): if not node: return # Pattern: Recursive Postorder (Left, Right, Root) traverse(node.left) traverse(node.right) result.append(node.val) traverse(root) return result
Time Complexity: We visit every node exactly once. At each node, we perform constant time operations (null check, two function calls, and one list insertion). Thus, the time complexity is linear with respect to the number of nodes .
Space Complexity: The space complexity is determined by the recursion stack depth. In the worst case (a skewed tree resembling a linked list), the recursion depth is . In the best case (a balanced tree), the depth is . Additionally, the output list takes space, but auxiliary stack space is the primary algorithmic consideration.
The Tree DFS - Recursive Postorder Traversal pattern is a foundational building block for many advanced tree problems.
This usually happens if you forget the base case. In the recursive pattern, you must explicitly check if node is null (or None) and return immediately. Without this, the function calls itself infinitely on null pointers.
This is a classic confusion between Preorder and Postorder traversals.
result.add(node.val) happens after both traverse(node.left) and traverse(node.right) have returned.Creating a new list at every step (e.g., return postorder(left) + postorder(right) + [val]) causes behavior due to list copying.