Files
codetutor/backend/data/questions/binary-tree-postorder-traversal.yaml

241 lines
9.9 KiB
YAML

title: Binary Tree Postorder Traversal
slug: binary-tree-postorder-traversal
difficulty: easy
leetcode_id: 145
leetcode_url: https://leetcode.com/problems/binary-tree-postorder-traversal/
categories:
- trees
- stack
- recursion
patterns:
- slug: dfs
is_optimal: true
- slug: tree-traversal
is_optimal: false
function_signature: "def postorder_traversal(root: TreeNode) -> list[int]:"
test_cases:
visible:
- input: { root: [1, null, 2, 3] }
expected: [3, 2, 1]
- input: { root: [] }
expected: []
- input: { root: [1] }
expected: [1]
hidden:
- input: { root: [1, 2, 3, 4, 5, null, 8, null, null, 6, 7, 9] }
expected: [4, 6, 7, 5, 2, 9, 8, 3, 1]
- input: { root: [1, 2, null, 3, null, 4] }
expected: [4, 3, 2, 1]
- input: { root: [1, null, 2, null, 3] }
expected: [3, 2, 1]
- input: { root: [5, 3, 7, 2, 4, 6, 8] }
expected: [2, 4, 3, 6, 8, 7, 5]
description: |
Given the `root` of a binary tree, return *the postorder traversal of its nodes' values*.
**Postorder traversal** visits nodes in the order: **left subtree → right subtree → root**.
examples:
- input: "root = [1,null,2,3]"
output: "[3,2,1]"
explanation: "The tree has structure: 1 -> right: 2 -> left: 3. Postorder visits 3 first (leftmost leaf), then 2, then 1 (root)."
- input: "root = [1,2,3,4,5,null,8,null,null,6,7,9]"
output: "[4,6,7,5,2,9,8,3,1]"
explanation: "Visit all left descendants first, then right descendants, then roots - working bottom-up."
- input: "root = []"
output: "[]"
explanation: "Empty tree returns empty list."
- input: "root = [1]"
output: "[1]"
explanation: "Single node tree returns just that node."
constraints: |
- `0 <= number of nodes <= 100`
- `-100 <= Node.val <= 100`
explanation:
intuition: |
Think of postorder traversal as **processing children before their parent**. Imagine you're a postal worker who must deliver packages to every house in a neighbourhood, but with one rule: you can only mark a house as "visited" after you've visited all houses in its left and right branches.
The order is: **Left → Right → Root**. This means for any node, you first completely explore its left subtree, then completely explore its right subtree, and only then do you process the node itself.
Why is this useful? Postorder is the natural order for **bottom-up computations** - calculating subtree heights, deleting trees (delete children before parent), or evaluating expression trees (evaluate operands before operators).
The recursive approach mirrors this definition directly: recurse left, recurse right, then add the current node. The iterative approach is trickier because we need to remember whether we've already processed a node's children.
approach: |
We'll cover both the **Recursive** and **Iterative** approaches:
**Recursive Approach**
**Step 1: Define the base case**
- If the current node is `None`, return immediately (nothing to process)
&nbsp;
**Step 2: Recurse in postorder**
- First, recursively traverse the left subtree
- Then, recursively traverse the right subtree
- Finally, add the current node's value to the result
&nbsp;
**Iterative Approach (Two Stacks / Reverse Pre-order)**
The trick is to recognise that postorder (Left → Right → Root) is the **reverse** of a modified preorder (Root → Right → Left).
**Step 1: Use a stack for traversal**
- Push the root onto the stack
- Pop a node, add its value to the result
- Push left child first, then right child (so right is processed first)
&nbsp;
**Step 2: Reverse the result**
- The traversal gives us Root → Right → Left
- Reversing gives us Left → Right → Root (postorder!)
&nbsp;
Alternatively, you can use a single stack with a "last visited" pointer to track whether you're returning from a left or right child, but the two-stack method is more intuitive.
common_pitfalls:
- title: Confusing Traversal Orders
description: |
The three DFS traversals differ only in *when* you process the current node:
- **Preorder**: Root → Left → Right (process node first)
- **Inorder**: Left → Root → Right (process node in middle)
- **Postorder**: Left → Right → Root (process node last)
A common mistake is mixing up the order. Remember: "post" means "after" - process the node *after* its children.
wrong_approach: "Adding node value before recursing on children"
correct_approach: "Add node value after both recursive calls complete"
- title: Iterative Postorder is Harder
description: |
Unlike preorder (which maps directly to a stack), postorder requires extra bookkeeping. You need to know when you're "done" with a node's children before processing it.
The simplest iterative solution uses the reverse-preorder trick: do Root → Right → Left traversal, then reverse. Trying to do true iterative postorder with one stack requires tracking the "last visited" node.
wrong_approach: "Using the same pattern as iterative preorder"
correct_approach: "Use reverse preorder trick or track last visited node"
- title: Forgetting the Empty Tree Case
description: |
When `root` is `None`, you should return an empty list `[]`. Forgetting this base case causes `AttributeError` when trying to access `root.val`.
wrong_approach: "Assuming root is always valid"
correct_approach: "Check for None at the start"
key_takeaways:
- "**Traversal order matters**: Postorder (Left → Right → Root) processes children before parents - essential for bottom-up tree computations"
- "**Recursive vs Iterative**: Recursion naturally matches tree structure, but iterative solutions use explicit stacks to simulate the call stack"
- "**Reverse preorder trick**: Postorder is the reverse of modified preorder (Root → Right → Left) - a clever way to avoid complex bookkeeping"
- "**Foundation for tree problems**: Understanding all three DFS traversals (pre/in/post) is essential for tree manipulation problems"
time_complexity: "O(n). Each node is visited exactly once, where `n` is the number of nodes in the tree."
space_complexity: "O(h) for recursion or O(n) for iterative. The recursive call stack uses space proportional to tree height `h`. In the worst case (skewed tree), `h = n`. The iterative approach uses O(n) for the output list and O(h) for the stack."
solutions:
- approach_name: Recursive DFS
is_optimal: true
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def postorder_traversal(root: TreeNode | None) -> list[int]:
result = []
def dfs(node: TreeNode | None) -> None:
if node is None:
return
# Postorder: Left -> Right -> Root
dfs(node.left) # Process entire left subtree first
dfs(node.right) # Then process entire right subtree
result.append(node.val) # Finally, add current node
dfs(root)
return result
explanation: |
**Time Complexity:** O(n) — Visit each node exactly once.
**Space Complexity:** O(h) — Recursion stack depth equals tree height. Worst case O(n) for skewed tree, O(log n) for balanced tree.
The recursive solution directly implements the postorder definition: process left subtree, then right subtree, then current node. The call stack implicitly handles the order.
- approach_name: Iterative (Reverse Preorder)
is_optimal: true
code: |
def postorder_traversal(root: TreeNode | None) -> list[int]:
if root is None:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# Push left first, then right
# So right is popped first -> gives Root, Right, Left order
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
# Reverse to get Left, Right, Root (postorder)
return result[::-1]
explanation: |
**Time Complexity:** O(n) — Visit each node once, plus O(n) for reversal.
**Space Complexity:** O(n) — Stack holds at most O(h) nodes, result list holds n values.
This approach exploits a clever observation: postorder (L→R→Root) is the reverse of modified preorder (Root→R→L). We do the modified preorder using a stack, then reverse the result.
- approach_name: Iterative (Single Stack with Tracking)
is_optimal: false
code: |
def postorder_traversal(root: TreeNode | None) -> list[int]:
if root is None:
return []
result = []
stack = []
current = root
last_visited = None
while stack or current:
# Go as far left as possible
while current:
stack.append(current)
current = current.left
# Peek at the top node
node = stack[-1]
# If right child exists and we haven't visited it yet
if node.right and node.right != last_visited:
current = node.right
else:
# Process the node - both children are done
result.append(node.val)
last_visited = stack.pop()
return result
explanation: |
**Time Complexity:** O(n) — Each node is pushed and popped once.
**Space Complexity:** O(h) — Stack depth equals tree height.
This is the "true" iterative postorder without reversing. We track the last visited node to know whether we're returning from the left or right child. Only process a node when both its children are done.