Files
codetutor/backend/data/questions/binary-tree-inorder-traversal.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)
&nbsp;
**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
&nbsp;
**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
&nbsp;
**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
&nbsp;
**Step 3: Return the result**
- The `result` list now contains values in inorder sequence
&nbsp;
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.