Files
codetutor/backend/data/questions/construct-binary-tree-from-preorder-and-inorder-traversal.yaml

252 lines
10 KiB
YAML

title: Construct Binary Tree from Preorder and Inorder Traversal
slug: construct-binary-tree-from-preorder-and-inorder-traversal
difficulty: medium
leetcode_id: 105
leetcode_url: https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
categories:
- trees
- arrays
- hash-tables
patterns:
- dfs
- tree-traversal
function_signature: "def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:"
test_cases:
visible:
- input: { preorder: [3, 9, 20, 15, 7], inorder: [9, 3, 15, 20, 7] }
expected: [3, 9, 20, null, null, 15, 7]
- input: { preorder: [-1], inorder: [-1] }
expected: [-1]
hidden:
- input: { preorder: [1, 2], inorder: [2, 1] }
expected: [1, 2]
- input: { preorder: [1, 2], inorder: [1, 2] }
expected: [1, null, 2]
- input: { preorder: [1, 2, 3], inorder: [2, 1, 3] }
expected: [1, 2, 3]
- input: { preorder: [1, 2, 4, 5, 3, 6, 7], inorder: [4, 2, 5, 1, 6, 3, 7] }
expected: [1, 2, 3, 4, 5, 6, 7]
- input: { preorder: [5, 4, 3, 2, 1], inorder: [1, 2, 3, 4, 5] }
expected: [5, 4, null, 3, null, 2, null, 1]
description: |
Given two integer arrays `preorder` and `inorder` where `preorder` is the preorder traversal of a binary tree and `inorder` is the inorder traversal of the same tree, construct and return *the binary tree*.
constraints: |
- `1 <= preorder.length <= 3000`
- `inorder.length == preorder.length`
- `-3000 <= preorder[i], inorder[i] <= 3000`
- `preorder` and `inorder` consist of **unique** values
- Each value of `inorder` also appears in `preorder`
- `preorder` is **guaranteed** to be the preorder traversal of the tree
- `inorder` is **guaranteed** to be the inorder traversal of the tree
examples:
- input: "preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]"
output: "[3,9,20,null,null,15,7]"
explanation: "The root is 3, with left subtree rooted at 9 and right subtree rooted at 20, which has children 15 and 7."
- input: "preorder = [-1], inorder = [-1]"
output: "[-1]"
explanation: "A single node tree with value -1."
explanation:
intuition: |
Think of this problem as a **detective puzzle** where you have two different witnesses describing the same family tree from different perspectives.
The key insight lies in understanding what each traversal tells us:
- **Preorder** visits: `root -> left subtree -> right subtree`. This means the **first element is always the root**.
- **Inorder** visits: `left subtree -> root -> right subtree`. This means once we know the root, **everything to its left belongs to the left subtree**, and everything to its right belongs to the right subtree.
Imagine the inorder array as a "splitter". Once we identify the root from preorder, we can find it in inorder to **partition** the elements into left and right subtrees. Then we recursively apply the same logic to each subtree.
For example, with `preorder = [3,9,20,15,7]` and `inorder = [9,3,15,20,7]`:
1. Root is `3` (first in preorder)
2. In inorder, `3` is at index 1, so left subtree has `[9]` and right subtree has `[15,20,7]`
3. Recursively build left subtree (root `9`) and right subtree (root `20`)
This divide-and-conquer approach naturally constructs the tree from top to bottom.
approach: |
We solve this using **Divide and Conquer with Hash Map Optimisation**:
**Step 1: Build a lookup map**
- Create a hash map from `value -> index` for the inorder array
- This allows O(1) lookup of any value's position in inorder
- Without this, we'd need O(n) search each time, making the solution O(n^2)
&nbsp;
**Step 2: Define recursive build function**
- Parameters track the current range in both preorder and inorder arrays
- `pre_start`, `pre_end`: bounds in preorder array
- `in_start`, `in_end`: bounds in inorder array
- Base case: if `pre_start > pre_end`, return `None` (empty subtree)
&nbsp;
**Step 3: Identify root and partition**
- The root value is `preorder[pre_start]` (first element in current preorder range)
- Find root's index in inorder using the hash map: `root_idx`
- Calculate left subtree size: `left_size = root_idx - in_start`
&nbsp;
**Step 4: Recursively build subtrees**
- **Left subtree**:
- Preorder range: `[pre_start + 1, pre_start + left_size]`
- Inorder range: `[in_start, root_idx - 1]`
- **Right subtree**:
- Preorder range: `[pre_start + left_size + 1, pre_end]`
- Inorder range: `[root_idx + 1, in_end]`
&nbsp;
**Step 5: Connect and return**
- Create the root node with the identified value
- Attach left and right children from recursive calls
- Return the root node
common_pitfalls:
- title: Linear Search in Inorder
description: |
A common mistake is searching for the root in inorder array with a loop each time:
```python
root_idx = inorder.index(root_val) # O(n) each call!
```
With recursion depth of O(n) and O(n) search per level, this gives **O(n^2)** time complexity. For `n = 3000`, this is 9 million operations and may TLE.
Solution: Pre-build a hash map for O(1) lookups.
wrong_approach: "Linear search for root index each recursion"
correct_approach: "Hash map for O(1) index lookup"
- title: Incorrect Range Calculations
description: |
The trickiest part is correctly computing the preorder ranges for subtrees. The left subtree in preorder starts right after the root (`pre_start + 1`) and spans `left_size` elements.
Common error: Using inorder indices for preorder ranges, or off-by-one errors in calculating where the right subtree begins.
Draw out a small example and trace the indices carefully.
wrong_approach: "Confusing inorder indices with preorder indices"
correct_approach: "Calculate left_size from inorder, apply to preorder ranges"
- title: Forgetting Base Case
description: |
Without a proper base case, recursion continues infinitely. When `pre_start > pre_end` (or equivalently `in_start > in_end`), the current subtree is empty and should return `None`.
Some implementations use array slicing instead of indices, which handles this naturally but uses O(n) extra space per call.
key_takeaways:
- "**Traversal properties**: Preorder's first element is always root; inorder partitions left/right subtrees"
- "**Divide and conquer**: Break the problem into smaller subproblems (left and right subtrees) and combine results"
- "**Hash map optimisation**: Pre-compute lookups to reduce O(n^2) to O(n)"
- "**Foundation for similar problems**: Same technique applies to constructing from postorder + inorder, or serialisation/deserialisation"
time_complexity: "O(n). Each node is visited exactly once, and hash map lookups are O(1)."
space_complexity: "O(n). The hash map stores n elements, and recursion stack can be O(n) in worst case (skewed tree)."
solutions:
- approach_name: Divide and Conquer with Hash Map
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 build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
# Build hash map for O(1) index lookup in inorder
inorder_map = {val: idx for idx, val in enumerate(inorder)}
def build(pre_start: int, pre_end: int, in_start: int, in_end: int) -> TreeNode | None:
# Base case: empty subtree
if pre_start > pre_end:
return None
# Root is first element in current preorder range
root_val = preorder[pre_start]
root = TreeNode(root_val)
# Find root position in inorder (O(1) lookup)
root_idx = inorder_map[root_val]
# Calculate size of left subtree
left_size = root_idx - in_start
# Recursively build left subtree
# Preorder: elements after root, spanning left_size
# Inorder: elements before root
root.left = build(
pre_start + 1, pre_start + left_size,
in_start, root_idx - 1
)
# Recursively build right subtree
# Preorder: elements after left subtree
# Inorder: elements after root
root.right = build(
pre_start + left_size + 1, pre_end,
root_idx + 1, in_end
)
return root
return build(0, len(preorder) - 1, 0, len(inorder) - 1)
explanation: |
**Time Complexity:** O(n) — Each node processed once with O(1) hash map lookup.
**Space Complexity:** O(n) — Hash map stores n entries; recursion stack up to O(n) for skewed trees.
The hash map transforms what would be O(n) searches into O(1) lookups, making this the optimal approach. The recursive structure naturally mirrors the tree's hierarchy.
- approach_name: Divide and Conquer with Array Slicing
is_optimal: false
code: |
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
# Base case: empty arrays
if not preorder or not inorder:
return None
# Root is first element of preorder
root_val = preorder[0]
root = TreeNode(root_val)
# Find root in inorder (O(n) search)
root_idx = inorder.index(root_val)
# Elements before root_idx in inorder are left subtree
# Elements after root_idx in inorder are right subtree
# Slice preorder accordingly (skip first element which is root)
root.left = build_tree(
preorder[1:root_idx + 1],
inorder[:root_idx]
)
root.right = build_tree(
preorder[root_idx + 1:],
inorder[root_idx + 1:]
)
return root
explanation: |
**Time Complexity:** O(n^2) — Linear search in inorder at each level, with n levels worst case.
**Space Complexity:** O(n^2) — Array slicing creates new arrays at each recursion level.
This approach is more intuitive and readable, making it good for understanding the algorithm. However, the linear search and array slicing make it less efficient. For small inputs it works fine, but may TLE on larger test cases.