Skip to content

105. Construct Binary Tree from Preorder and Inorder Traversal (Medium)

Problem

Given two integer arrays preorder and inorder representing the preorder and inorder traversals of a binary tree (assume unique values), reconstruct and return the tree.

Example

  • preorder = [3,9,20,15,7], inorder = [9,3,15,20,7][3,9,20,null,null,15,7]
  • preorder = [-1], inorder = [-1][-1]

LeetCode 105 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Recursive with list.index per call

First element of preorder is the root. Find it in inorder to split left/right subtrees. Recurse.

def build_tree(preorder, inorder):
if not preorder:
return None
root_val = preorder[0] # L1: O(1)
root = TreeNode(root_val)
mid = inorder.index(root_val) # L2: O(n) linear scan
root.left = build_tree(preorder[1:mid + 1], inorder[:mid]) # L3: O(n) slice
root.right = build_tree(preorder[mid + 1:], inorder[mid + 1:]) # L4: O(n) slice
return root

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L1 (read first)O(1)O(1)nO(n)O(n)
L2 (list.index)O(n)O(n)nO(n2)O(n²) ← dominates
L3/L4 (slicing)O(n)O(n)nO(n2)O(n²)

Both L2 and L3/L4 are O(n)O(n) per call, and both are called O(n)O(n) times total. The n² comes from re-scanning the inorder array at every level of recursion.

Complexity

  • Time: O(n2)O(n²). list.index is O(n)O(n); called O(n)O(n) times.
  • Space: O(n2)O(n²) across slices; O(n)O(n) for the tree.

Approach 2: Hash map inorder index + pointer recursion (optimal)

Precompute val → index in inorder once (O(n)O(n)). Pass index ranges into the recursion instead of slicing.

def build_tree(preorder, inorder):
inorder_idx = {v: i for i, v in enumerate(inorder)} # L1: O(n) build map
pre_i = [0]
def rec(in_l, in_r):
if in_l > in_r:
return None
root_val = preorder[pre_i[0]] # L2: O(1) index into preorder
pre_i[0] += 1 # L3: O(1) advance pointer
root = TreeNode(root_val)
mid = inorder_idx[root_val] # L4: O(1) hash lookup
root.left = rec(in_l, mid - 1) # L5: recurse left subtree
root.right = rec(mid + 1, in_r) # L6: recurse right subtree
return root
return rec(0, len(inorder) - 1)

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L1 (build map)O(n)O(n)1O(n)O(n)
L2 (read preorder)O(1)O(1)nO(n)O(n)
L3 (advance pointer)O(1)O(1)nO(n)O(n)
L4 (hash lookup)O(1)O(1)nO(n)O(n) ← dominates (all per-node lines tie)
L5/L6 (recurse)O(1)O(1) dispatchnO(n)O(n)

Every node is constructed in O(1)O(1) given the precomputed map. No slicing, no scanning.

Complexity

  • Time: O(n)O(n). Each node constructed in O(1)O(1) given the index.
  • Space: O(n)O(n) for the map + O(h)O(h) recursion.

Why the preorder index is shared state

We consume the preorder from left to right, but we must process the left subtree completely before starting the right subtree. By sharing a single counter (pre_i) across recursive calls, the left subtree’s exhaustion naturally positions us at the right subtree’s root.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Iterative with a stack

More code; rarely preferred unless recursion depth is a specific concern. The iterative form pushes preorder values while walking the inorder until the top matches; then pops and assigns to the next preorder root.

def build_tree(preorder, inorder):
if not preorder:
return None
root = TreeNode(preorder[0])
stack = [root]
in_idx = 0
for i in range(1, len(preorder)):
node = TreeNode(preorder[i])
if stack[-1].val != inorder[in_idx]:
# Top hasn't been "completed" by inorder yet → current is its left child
stack[-1].left = node
else:
# Pop while top matches inorder cursor (we've walked past their subtrees)
popped = None
while stack and stack[-1].val == inorder[in_idx]:
popped = stack.pop()
in_idx += 1
# The last popped node is the parent whose right subtree starts here
popped.right = node
stack.append(node)
return root

The invariant: the stack holds the current path of “left-child ancestors.” When the top matches the inorder cursor, we’ve finished a left subtree, and the next preorder value belongs to a right child further up the path. The pop-while loop walks up to find which one.

Complexity

  • Time: O(n)O(n). Each node is pushed and popped once.
  • Space: O(n)O(n) for the stack.

Summary

ApproachTimeSpace
Recursive with list.indexO(n2)O(n²)O(n2)O(n²) slices
Hash map + recursionO(n)O(n)O(n)O(n)
Iterative stackO(n)O(n)O(n)O(n)

The hash-map + recursion pattern generalizes to problem 106 (Build from Inorder + Postorder), same code with the preorder cursor replaced by a right-to-left postorder cursor.

Test cases

class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(preorder, inorder):
inorder_idx = {v: i for i, v in enumerate(inorder)}
pre_i = [0]
def rec(in_l, in_r):
if in_l > in_r:
return None
root_val = preorder[pre_i[0]]
pre_i[0] += 1
root = TreeNode(root_val)
mid = inorder_idx[root_val]
root.left = rec(in_l, mid - 1)
root.right = rec(mid + 1, in_r)
return root
return rec(0, len(inorder) - 1)
def tree_to_list(root):
"""Level-order serialize for comparison."""
if not root:
return []
from collections import deque
result, q = [], deque([root])
while q:
node = q.popleft()
if node:
result.append(node.val)
q.append(node.left)
q.append(node.right)
else:
result.append(None)
# strip trailing Nones
while result and result[-1] is None:
result.pop()
return result
def _run_tests():
# example from problem
t = build_tree([3, 9, 20, 15, 7], [9, 3, 15, 20, 7])
assert tree_to_list(t) == [3, 9, 20, None, None, 15, 7]
# single node
t2 = build_tree([-1], [-1])
assert t2.val == -1
assert t2.left is None and t2.right is None
# left-skewed
t3 = build_tree([1, 2, 3], [3, 2, 1])
assert tree_to_list(t3) == [1, 2, None, 3]
# right-skewed
t4 = build_tree([1, 2, 3], [1, 2, 3])
assert tree_to_list(t4) == [1, None, 2, None, 3]
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Divide and Conquer, the split, solve, and combine pattern for independent subproblems.
  • Tree Traversal, the recursive or iterative visit pattern for carrying path and subtree state.