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 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)   **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)   **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`   **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]`   **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.