105. Construct Binary Tree From Preorder And Inorder Traversal - Explanation
Description
You are given two integer arrays preorder and inorder.
preorderis the preorder traversal of a binary treeinorderis the inorder traversal of the same tree- Both arrays are of the same size and consist of unique values.
Rebuild the binary tree from the preorder and inorder traversals and return its root.
Example 1:
Input: preorder = [1,2,3,4], inorder = [2,1,3,4]
Output: [1,2,3,null,null,null,4]Example 2:
Input: preorder = [1], inorder = [1]
Output: [1]Constraints:
1 <= inorder.length <= 1000.inorder.length == preorder.length-1000 <= preorder[i], inorder[i] <= 1000
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(n) time and O(n) space, where n is the number of nodes.
Hint 1
You can observe that the in-order traversal helps divide the array into two halves based on the root node: the left part corresponds to the left subtree, and the right part corresponds to the right subtree. Can you think of how we can get the index of the root node in the in-order array? Maybe you should look into the pre-order traversal array.
Hint 2
From the pre-order traversal, we know that the first node is the root node. Using this information, can you now construct the binary tree?
Hint 3
After getting the root node from pre-order traversal, we then look for the index of that node in the in-order array. We can linearly search for the index but this would be an O(n^2) solution. Can you think of a better way? Maybe we can use a data structure to get the index of a node in O(1)?
Hint 4
We can use a hash map to get the index of any node in the in-order array in O(1) time. How can we implement this?
Hint 5
We use Depth First Search (DFS) to construct the tree. A global variable tracks the current index in the pre-order array. Indices l and r represent the segment in the in-order array for the current subtree. For each node in the pre-order array, we create a node, find its index in the in-order array using the hash map, and recursively build the left and right subtrees by splitting the range [l, r] into two parts for the left and right subtrees.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Binary Tree Structure - Understanding how nodes connect via left and right children
- Tree Traversal Orders - Knowing what preorder (root-left-right) and inorder (left-root-right) sequences represent
- Recursion / DFS - Building solutions by breaking problems into subproblems on left and right subtrees
- Hash Maps - Using dictionaries for O(1) index lookups to optimize from O(n^2) to O(n)
1. Depth First Search
Intuition
The first element of the preorder array is always the root. We can find this root's position in the inorder array, which divides inorder into left and right subtrees. Elements before the root in inorder belong to the left subtree, and elements after belong to the right subtree. The same split applies to preorder. We recursively build left and right subtrees using the corresponding portions of both arrays.
Algorithm
- If either array is empty, return
null(base case). - Create a root node with the first element of
preorder. - Find the index of the root value in
inorder(call itmid). - Recursively build the left subtree using
preorder[1:mid+1]andinorder[0:mid]. - Recursively build the right subtree using
preorder[mid+1:]andinorder[mid+1:]. - Return the root node.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder or not inorder:
return None
root = TreeNode(preorder[0])
mid = inorder.index(preorder[0])
root.left = self.buildTree(preorder[1 : mid + 1], inorder[:mid])
root.right = self.buildTree(preorder[mid + 1 :], inorder[mid + 1 :])
return rootTime & Space Complexity
- Time complexity:
- Space complexity:
2. Hash Map + Depth First Search
Intuition
In the basic DFS approach, we search for the root's position in inorder using linear search, which takes O(n) time per node. By precomputing a hash map from values to their indices in inorder, we can find the root's position in O(1) time. We also avoid creating new arrays by passing indices that define the current subarray boundaries.
Algorithm
- Build a hash map mapping each value in
inorderto its index. - Maintain a global preorder index starting at
0. - Define a recursive function
dfs(l, r)for theinorderrange[l, r]. - If
l > r, returnnull(base case). - Get the root value from
preorderat the current index, increment the index. - Look up the root's position in the hash map (call it
mid). - Recursively build left subtree with
dfs(l, mid-1)and right subtree withdfs(mid+1, r). - Return the root node.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
indices = {val: idx for idx, val in enumerate(inorder)}
self.pre_idx = 0
def dfs(l, r):
if l > r:
return None
root_val = preorder[self.pre_idx]
self.pre_idx += 1
root = TreeNode(root_val)
mid = indices[root_val]
root.left = dfs(l, mid - 1)
root.right = dfs(mid + 1, r)
return root
return dfs(0, len(inorder) - 1)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Depth First Search (Optimal)
Intuition
We can avoid the hash map entirely by using a limit-based approach. Instead of explicitly finding the root's position, we pass a "limit" value that tells us when to stop building the left subtree. When we encounter the limit value in inorder, we know the left subtree is complete. The preorder index tells us which node to create next, and the inorder index tells us when we have finished a subtree.
Algorithm
- Maintain two global indices:
preIdxforpreorderandinIdxforinorder. - Define a recursive function
dfs(limit)that builds a subtree until it hits the limit value. - If
preIdx >= n, returnnull(no more nodes). - If
inorder[inIdx] == limit, incrementinIdxand returnnull(subtree complete). - Create a root node with
preorder[preIdx], incrementpreIdx. - Build the left subtree with
dfs(root.val)since nodes less than root appear before it ininorder. - Build the right subtree with
dfs(limit)using the original limit. - Return the root node. Start with
dfs(infinity)or a value larger than any node.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
preIdx = inIdx = 0
def dfs(limit):
nonlocal preIdx, inIdx
if preIdx >= len(preorder):
return None
if inorder[inIdx] == limit:
inIdx += 1
return None
root = TreeNode(preorder[preIdx])
preIdx += 1
root.left = dfs(root.val)
root.right = dfs(limit)
return root
return dfs(float('inf'))Time & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
4. Morris Traversal
Intuition
Morris traversal allows us to build the tree iteratively without using a recursion stack. The idea is to use the right pointers of nodes to temporarily store parent references, simulating the call stack. We build nodes as we iterate through preorder, connecting them via left/right pointers. When we finish a left subtree (detected by matching the inorder sequence), we restore the original structure by clearing temporary links and moving up the tree.
Algorithm
- Create a dummy head node and set
currto point to it. - Iterate through
preorderwith indexiandinorderwith indexj. - Create a new node for
preorder[i]and attach it ascurr's right child, then movecurrto this new node. - While
preorder[i]does not matchinorder[j], keep creating left children (storing parent in right pointer). - When a match is found, increment
j. Whilecurr.rightexists and matchesinorder[j], clear the temporary right link and move up. - Continue until all nodes are processed.
- Return
head.rightas the actual root.
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
head = TreeNode(None)
curr = head
i, j, n = 0, 0, len(preorder)
while i < n and j < n:
# Go right and then as far left as possible
curr.right = TreeNode(preorder[i], right = curr.right)
curr = curr.right
i += 1
while i < n and curr.val != inorder[j]:
curr.left = TreeNode(preorder[i], right=curr)
curr = curr.left
i += 1
j += 1
while curr.right and j < n and curr.right.val == inorder[j]:
prev = curr.right
curr.right = None
curr = prev
j += 1
return head.rightTime & Space Complexity
- Time complexity:
- Space complexity:
- extra space.
- for the output tree.
Common Pitfalls
Off-by-One Error When Splitting Arrays
The split point mid represents the root's index in inorder. When slicing preorder, the left subtree uses preorder[1:mid+1] (not preorder[1:mid]), since there are exactly mid elements in the left subtree.
# Wrong: preorder[1:mid]
# Correct: preorder[1:mid+1]Building Right Subtree Before Left Subtree
When using a global preorder index, the left subtree must be built first. Preorder visits root, then left, then right. Building right first consumes wrong nodes from the preorder array.
Confusing Preorder and Inorder Roles
The root always comes from preorder (first element), while the split point is found in inorder. Swapping these roles produces an incorrect tree structure.
Sign in to join the discussion