237 lines
9.8 KiB
YAML
237 lines
9.8 KiB
YAML
title: Binary Tree Inorder Traversal
|
|
slug: binary-tree-inorder-traversal
|
|
difficulty: easy
|
|
leetcode_id: 94
|
|
leetcode_url: https://leetcode.com/problems/binary-tree-inorder-traversal/
|
|
categories:
|
|
- trees
|
|
- stack
|
|
- recursion
|
|
patterns:
|
|
- tree-traversal
|
|
- dfs
|
|
|
|
description: |
|
|
Given the `root` of a binary tree, return *the inorder traversal of its nodes' values*.
|
|
|
|
**Inorder traversal** visits nodes in the order: **left subtree**, then **root**, then **right subtree**.
|
|
|
|
For a Binary Search Tree (BST), inorder traversal returns nodes in sorted ascending order.
|
|
|
|
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,3,2]"
|
|
explanation: "The tree has root 1, with right child 2, which has left child 3. Inorder visits: 1 (no left child), then 3 (left of 2), then 2."
|
|
- input: "root = [1,2,3,4,5,null,8,null,null,6,7,9]"
|
|
output: "[4,2,6,5,7,1,3,9,8]"
|
|
explanation: "Full traversal of a more complex tree, visiting left subtree first, then root, then right subtree at each level."
|
|
- 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 inorder traversal like reading a book where you must finish all the pages on the left before reading the current page, then read all the pages on the right.
|
|
|
|
Imagine you're standing at the root of the tree. Before you can "visit" (record) the current node, you must first completely explore everything to your left. Only after exhausting the left subtree do you record the current node, then move to explore the right.
|
|
|
|
This **left-root-right** pattern has a beautiful property for Binary Search Trees: it visits nodes in **sorted order**. This is because in a BST, all left descendants are smaller and all right descendants are larger than the current node.
|
|
|
|
The key insight is that this naturally recursive pattern can also be implemented iteratively using a **stack** to simulate the call stack. The stack helps us remember which nodes we still need to "come back to" after exploring their left subtrees.
|
|
|
|
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 inorder pattern**
|
|
|
|
- Recursively traverse the left subtree
|
|
- Add the current node's value to the result
|
|
- Recursively traverse the right subtree
|
|
|
|
|
|
|
|
**Iterative Approach:**
|
|
|
|
**Step 1: Initialise data structures**
|
|
|
|
- `result`: Empty list to store the traversal order
|
|
- `stack`: Empty stack to track nodes we need to return to
|
|
- `current`: Pointer starting at the root
|
|
|
|
|
|
|
|
**Step 2: Traverse using the stack**
|
|
|
|
- While `current` is not None OR stack is not empty:
|
|
- Go as far left as possible, pushing each node onto the stack
|
|
- When you can't go left anymore, pop from stack
|
|
- Add the popped node's value to result (this is the "visit")
|
|
- Move to the right child and repeat
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- The `result` list now contains values in inorder sequence
|
|
|
|
|
|
|
|
The iterative approach mimics exactly what the recursive approach does, but uses an explicit stack instead of the implicit call stack.
|
|
|
|
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 first)
|
|
- **Inorder**: left, root, right (process node in middle)
|
|
- **Postorder**: left, right, root (process node last)
|
|
|
|
For inorder, remember "**in** the middle" — the root is visited **in** between left and right.
|
|
wrong_approach: "Visiting root before left subtree"
|
|
correct_approach: "Always complete left subtree before visiting current node"
|
|
|
|
- title: Forgetting to Handle Empty Trees
|
|
description: |
|
|
An empty tree (root is `None`) should return an empty list `[]`, not cause an error.
|
|
|
|
Always check for the empty tree case, either as a base case in recursion or as an initial condition in iteration.
|
|
wrong_approach: "Assuming root always exists"
|
|
correct_approach: "Handle None root as base case returning empty list"
|
|
|
|
- title: Iterative Stack Logic Errors
|
|
description: |
|
|
In the iterative approach, a common mistake is not understanding when to push vs. pop:
|
|
|
|
- **Push**: When moving left, push current node (we'll come back to it)
|
|
- **Pop**: When we can't go left anymore, pop to visit the node
|
|
- **Move right**: After visiting, move to right child
|
|
|
|
The loop condition `while current or stack` is crucial — we continue if either there's a current node to process OR nodes waiting on the stack.
|
|
wrong_approach: "Only looping while current is not None"
|
|
correct_approach: "Loop while current exists OR stack is not empty"
|
|
|
|
key_takeaways:
|
|
- "**Inorder = left-root-right**: The middle position of 'in-order' helps remember that root is visited in the middle"
|
|
- "**BST property**: Inorder traversal of a BST yields sorted output — useful for validation and conversion problems"
|
|
- "**Stack simulates recursion**: Any recursive traversal can be converted to iterative using an explicit stack"
|
|
- "**Foundation problem**: This pattern extends to many tree problems like validating BSTs, finding kth smallest element, and tree serialisation"
|
|
|
|
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 inorder_traversal(root: TreeNode | None) -> list[int]:
|
|
result = []
|
|
|
|
def inorder(node: TreeNode | None) -> None:
|
|
# Base case: empty node
|
|
if node is None:
|
|
return
|
|
|
|
# Left subtree first
|
|
inorder(node.left)
|
|
# Visit current node (add to result)
|
|
result.append(node.val)
|
|
# Right subtree last
|
|
inorder(node.right)
|
|
|
|
inorder(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 inorder definition: traverse left, visit node, traverse right. The helper function captures results in the outer scope's `result` list.
|
|
|
|
- approach_name: Iterative with Stack
|
|
is_optimal: true
|
|
code: |
|
|
def inorder_traversal(root: TreeNode | None) -> list[int]:
|
|
result = []
|
|
stack = []
|
|
current = root
|
|
|
|
# Continue while there's a node to process or nodes on stack
|
|
while current or stack:
|
|
# Go as far left as possible
|
|
while current:
|
|
stack.append(current)
|
|
current = current.left
|
|
|
|
# Leftmost reached, pop and visit
|
|
current = stack.pop()
|
|
result.append(current.val)
|
|
|
|
# Move to right subtree
|
|
current = current.right
|
|
|
|
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 an explicit stack to simulate the recursive call stack. We push nodes while going left, pop to visit, then move right. This is the standard pattern for iterative tree traversal and is preferred in interviews when asked to avoid recursion.
|
|
|
|
- approach_name: Morris Traversal
|
|
is_optimal: false
|
|
code: |
|
|
def inorder_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:
|
|
# Create thread: link predecessor to current
|
|
predecessor.right = current
|
|
current = current.left
|
|
else:
|
|
# Thread exists, we've returned; remove it and visit
|
|
predecessor.right = None
|
|
result.append(current.val)
|
|
current = current.right
|
|
|
|
return result
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Though it looks like more work, 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 their successors). While optimal in space, it's more complex and modifies the tree during traversal. Useful when space is critical, but the iterative stack approach is usually preferred for clarity.
|