236 lines
9.8 KiB
YAML
236 lines
9.8 KiB
YAML
title: Binary Tree Preorder Traversal
|
|
slug: binary-tree-preorder-traversal
|
|
difficulty: easy
|
|
leetcode_id: 144
|
|
leetcode_url: https://leetcode.com/problems/binary-tree-preorder-traversal/
|
|
categories:
|
|
- trees
|
|
- stack
|
|
- recursion
|
|
patterns:
|
|
- tree-traversal
|
|
- dfs
|
|
|
|
description: |
|
|
Given the `root` of a binary tree, return *the preorder traversal of its nodes' values*.
|
|
|
|
**Preorder traversal** visits nodes in the order: **root**, then **left subtree**, then **right subtree**.
|
|
|
|
This is a depth-first traversal where we process the current node before exploring its children.
|
|
|
|
constraints: |
|
|
- The number of nodes in the tree is in the range `[0, 100]`
|
|
- `-100 <= Node.val <= 100`
|
|
|
|
examples:
|
|
- input: "root = [1,null,2,3]"
|
|
output: "[1,2,3]"
|
|
explanation: "The tree has root 1, with right child 2, which has left child 3. Preorder visits: 1 (root first), then no left child, then 2, then 3 (left of 2)."
|
|
- input: "root = [1,2,3,4,5,null,8,null,null,6,7,9]"
|
|
output: "[1,2,4,5,6,7,3,8,9]"
|
|
explanation: "Full traversal of a complex tree. Visit root 1, then fully traverse left subtree (2,4,5,6,7), then right subtree (3,8,9)."
|
|
- input: "root = []"
|
|
output: "[]"
|
|
explanation: "An empty tree returns an empty list."
|
|
- input: "root = [1]"
|
|
output: "[1]"
|
|
explanation: "A single node tree returns just that node's value."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of preorder traversal like exploring a cave system where you mark each chamber as soon as you enter it, before exploring any passages leading deeper.
|
|
|
|
Imagine you're a tour guide walking through a tree structure. Your rule is simple: **announce the current room immediately**, then explore all the rooms on the left, and finally explore all the rooms on the right.
|
|
|
|
The key insight is that "pre" in preorder means we process the node **before** (pre) its children. This makes preorder intuitive for tasks like:
|
|
- **Copying a tree**: You need to create a node before creating its children
|
|
- **Prefix expression evaluation**: Operators come before operands
|
|
- **Serialising a tree**: The root-first order makes deserialisation straightforward
|
|
|
|
Like all depth-first traversals, this recursive pattern can be converted to an iterative approach using a **stack**. The stack lets us remember which nodes still have unexplored right children after we've gone left.
|
|
|
|
approach: |
|
|
We present two approaches: **Recursive** (elegant and intuitive) and **Iterative** (uses explicit stack).
|
|
|
|
**Recursive Approach:**
|
|
|
|
**Step 1: Define the base case**
|
|
|
|
- If the current node is `None`, return (nothing to process)
|
|
|
|
|
|
|
|
**Step 2: Apply the preorder pattern**
|
|
|
|
- Add the current node's value to the result (visit first!)
|
|
- Recursively traverse the left subtree
|
|
- Recursively traverse the right subtree
|
|
|
|
|
|
|
|
**Iterative Approach:**
|
|
|
|
**Step 1: Initialise data structures**
|
|
|
|
- `result`: Empty list to store the traversal order
|
|
- `stack`: Initialise with the root node (if it exists)
|
|
|
|
|
|
|
|
**Step 2: Process nodes using the stack**
|
|
|
|
- While the stack is not empty:
|
|
- Pop a node from the stack
|
|
- Add its value to the result (this is the "visit")
|
|
- Push the **right child first**, then the **left child** (so left is processed first due to LIFO)
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- The `result` list now contains values in preorder sequence
|
|
|
|
|
|
|
|
The iterative approach pushes right before left because a stack is LIFO (Last In, First Out). Since we want to process left before right, we push right first so that left ends up on top.
|
|
|
|
common_pitfalls:
|
|
- title: Confusing Traversal Orders
|
|
description: |
|
|
There are three depth-first traversals, and mixing them up is common:
|
|
|
|
- **Preorder**: root, left, right (process node **pre**-children)
|
|
- **Inorder**: left, root, right (process node **in** the middle)
|
|
- **Postorder**: left, right, root (process node **post**-children)
|
|
|
|
For preorder, remember "**pre** means before" — visit the node **before** visiting children.
|
|
wrong_approach: "Visiting children before the current node"
|
|
correct_approach: "Always visit current node first, then left, then right"
|
|
|
|
- title: Wrong Stack Order in Iterative Solution
|
|
description: |
|
|
In the iterative approach, a critical mistake is pushing left child before right child onto the stack.
|
|
|
|
Since a stack is LIFO (Last In, First Out), the last item pushed is the first item popped. If we want to process left before right, we must push right first!
|
|
|
|
- Push right child → Push left child → Left gets popped first ✓
|
|
- Push left child → Push right child → Right gets popped first ✗
|
|
wrong_approach: "Pushing left child before right child"
|
|
correct_approach: "Push right child first, then left child"
|
|
|
|
- title: Forgetting to Handle Empty Trees
|
|
description: |
|
|
An empty tree (root is `None`) should return an empty list `[]`, not cause an error.
|
|
|
|
In the iterative approach, check if root is `None` before adding to the stack, or handle the empty stack case gracefully.
|
|
wrong_approach: "Assuming root always exists"
|
|
correct_approach: "Handle None root as base case returning empty list"
|
|
|
|
key_takeaways:
|
|
- "**Preorder = root-left-right**: The 'pre' prefix helps remember that the root is visited before (pre) its children"
|
|
- "**Stack order matters**: In iterative preorder, push right before left so that left is processed first (LIFO principle)"
|
|
- "**Tree copying pattern**: Preorder is ideal for tasks where you need to process a node before its children, like cloning a tree"
|
|
- "**Foundation problem**: Understanding preorder helps with tree serialisation, expression trees, and many recursive tree algorithms"
|
|
|
|
time_complexity: "O(n). We visit each of the `n` nodes exactly once."
|
|
space_complexity: "O(h) where `h` is the height of the tree. In the worst case (skewed tree), `h = n`, so O(n). For a balanced tree, O(log n)."
|
|
|
|
solutions:
|
|
- approach_name: Recursive
|
|
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 preorder_traversal(root: TreeNode | None) -> list[int]:
|
|
result = []
|
|
|
|
def preorder(node: TreeNode | None) -> None:
|
|
# Base case: empty node
|
|
if node is None:
|
|
return
|
|
|
|
# Visit current node first (preorder!)
|
|
result.append(node.val)
|
|
# Then traverse left subtree
|
|
preorder(node.left)
|
|
# Finally traverse right subtree
|
|
preorder(node.right)
|
|
|
|
preorder(root)
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is visited exactly once.
|
|
|
|
**Space Complexity:** O(h) — Recursive call stack depth equals tree height. Worst case O(n) for skewed tree, O(log n) for balanced tree.
|
|
|
|
The recursive approach directly expresses the preorder definition: visit node, traverse left, traverse right. The order of these three lines determines the traversal type.
|
|
|
|
- approach_name: Iterative with Stack
|
|
is_optimal: true
|
|
code: |
|
|
def preorder_traversal(root: TreeNode | None) -> list[int]:
|
|
if root is None:
|
|
return []
|
|
|
|
result = []
|
|
stack = [root]
|
|
|
|
while stack:
|
|
# Pop and visit the current node
|
|
node = stack.pop()
|
|
result.append(node.val)
|
|
|
|
# Push right first so left is processed first (LIFO)
|
|
if node.right:
|
|
stack.append(node.right)
|
|
if node.left:
|
|
stack.append(node.left)
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is pushed and popped exactly once.
|
|
|
|
**Space Complexity:** O(h) — Stack holds at most `h` nodes (the height of the tree).
|
|
|
|
This iterative version uses a stack to simulate recursion. The key insight is pushing right before left — since stacks are LIFO, this ensures left children are processed before right children. This is the cleanest iterative preorder implementation.
|
|
|
|
- approach_name: Morris Traversal
|
|
is_optimal: false
|
|
code: |
|
|
def preorder_traversal(root: TreeNode | None) -> list[int]:
|
|
result = []
|
|
current = root
|
|
|
|
while current:
|
|
if current.left is None:
|
|
# No left subtree, visit and go right
|
|
result.append(current.val)
|
|
current = current.right
|
|
else:
|
|
# Find inorder predecessor (rightmost in left subtree)
|
|
predecessor = current.left
|
|
while predecessor.right and predecessor.right != current:
|
|
predecessor = predecessor.right
|
|
|
|
if predecessor.right is None:
|
|
# First visit: add to result, create thread, go left
|
|
result.append(current.val)
|
|
predecessor.right = current
|
|
current = current.left
|
|
else:
|
|
# Thread exists, we've returned; remove thread and go right
|
|
predecessor.right = None
|
|
current = current.right
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each edge is traversed at most twice.
|
|
|
|
**Space Complexity:** O(1) — No stack or recursion; uses tree structure itself for navigation.
|
|
|
|
Morris traversal achieves O(1) space by temporarily modifying the tree (creating "threads" from predecessors back to current node). Unlike inorder Morris where we visit after removing the thread, in preorder we visit when first encountering the node (before creating the thread). More complex but useful when space is critical.
|