questions B (backspace - burst-balloons)

This commit is contained in:
2025-05-24 22:06:49 +01:00
parent 0b83eff6f8
commit c4662f5001
67 changed files with 13945 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
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)
&nbsp;
**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
&nbsp;
**Iterative Approach:**
**Step 1: Initialise data structures**
- `result`: Empty list to store the traversal order
- `stack`: Initialise with the root node (if it exists)
&nbsp;
**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)
&nbsp;
**Step 3: Return the result**
- The `result` list now contains values in preorder sequence
&nbsp;
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.