239 lines
9.8 KiB
YAML
239 lines
9.8 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:
|
|
- dfs
|
|
- tree-traversal
|
|
|
|
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)
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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)
|
|
|
|
|
|
|
|
**Step 2: Reverse the result**
|
|
|
|
- The traversal gives us Root → Right → Left
|
|
- Reversing gives us Left → Right → Root (postorder!)
|
|
|
|
|
|
|
|
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.
|