HARD Lab · Trees · 69

105. Construct Binary Tree from Preorder and Inorder Traversal

Preorder hands you the roots in order; inorder tells you what falls left vs right of each root. Consume preorder front-to-back as a pointer and split inorder around each root — an index map makes every split O(1).

MediumDivide & ConquerHash MapTypeScript

PROBLEM What we're solving

Given preorder and inorder traversals of a binary tree with unique values, rebuild the exact tree. preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] 3 is the root, with 9 on the left and 20 (15,7) on the right. Return that tree (its level-order form is [3,9,20,null,null,15,7]).

KEY IDEA Preorder = roots in order; inorder = the left/right partition

Insight → the firstelement of any preorder slice is that subtree's root. Locate that root inside the inorder slice: everything to its left is the left subtree, everything to its right is the right subtree. Because preorder is root, left…, right…, a single advancing pointerpreIdx over preorder always points at the next root — you never need to slice preorder, only inorder.

RECIPE Take a root, find it, split, recurse left then right

  • 0 · Pre-index inorder. Build Map<value, index>so "where is this root in inorder?" is O(1) instead of a linear scan.
  • 1 · Root = preorder front. Read preorder[preIdx++]; that value is the current subtree's root. The post-increment is what threads the roots in correct order.
  • 2 · Split inorder. Let mid = pos[root]. Inorder [lo..mid-1] is the left subtree; [mid+1..hi] is the right.
  • 3 · Recurse left, THEN right. Order matters: because preorder lists the entire left subtree before the right, you must consume the left recursion first so preIdx reaches the right root at the right time.
  • 4 · Base case. lo > hi ⇒ empty range ⇒ return null.
Classic confusion → swapping the recursion order (right before left) silently corrupts the tree: preIdx would grab the next root for the wrong side. The two recursive calls are NOT interchangeable here even though they look symmetric.

COST Complexity & alternatives

Scan inorder each call
O(n²)
A linear search for every root; degenerates on a skewed tree.
Inorder index map + pointer
O(n)
O(1) split per node, n nodes; O(n) space for the map + recursion.

Why the map matters

Without it, each recursion does an O(n) linear search for the root inside inorder, giving O(n²) overall (worst on a linear/skewed tree). The hash-map drops the per-node split to O(1), so total work is O(n). Recursion depth is the tree height, O(h), up to O(n) for a skewed tree.

Pattern transfer →the same "one traversal names the roots, another supplies the partition" idea solves Construct from Postorder + Inorder (LC 106 — read postorder back-to-front for roots) and Construct from Preorder + Postorder (LC 889). Slicing-vs-pointer is a recurring divide-and-conquer trick.

RUN IT Take a root from preorder, split inorder around it

step 0 / 12
STARTWalk preorder left to right. Each value is the root of the next subtree; split inorder around it into left and right.
preorder309120215374
State
0
preIdx
5 keys
inorder map
current root / split pointactive inorder [lo..hi]empty range → nullfinished
slowfast

TYPESCRIPT The solution, annotated

buildTree.ts
class TreeNode {
  val: number;
  left: TreeNode | null = null;
  right: TreeNode | null = null;
  constructor(val: number) {
    this.val = val;
  }
}

function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
  // Map each value to its index in inorder for O(1) splits.
  const pos = new Map<number, number>();
  inorder.forEach((v, i) => pos.set(v, i));

  let preIdx = 0; // next root to consume from preorder

  // Build the subtree whose inorder values are inorder[lo..hi].
  function build(lo: number, hi: number): TreeNode | null {
    if (lo > hi) return null;            // empty range → no node

    const rootVal = preorder[preIdx++]; // preorder front = this root
    const root = new TreeNode(rootVal);
    const mid = pos.get(rootVal)!;       // where root sits in inorder

    root.left = build(lo, mid - 1);      // left subtree first…
    root.right = build(mid + 1, hi);     // …because preorder is root,L,R
    return root;
  }

  return build(0, inorder.length - 1);
}

Reading it block by block

The index map. pos maps each value to its position in inorder. This is the optimization that turns the per-node split from an O(n) scan into an O(1) lookup.
The shared pointer. preIdx lives outside the recursion. Every call grabs the next root with preorder[preIdx++] — no need to pass or slice preorder ranges around.
Base case. lo > himeans the inorder window is empty, so this child doesn't exist → null. Don't touch preIdx here.
Split point. mid = pos.get(rootVal) is where the root sits in inorder. Left subtree = [lo, mid-1], right subtree = [mid+1, hi].
Left before right. Preorder is root, then the whole left subtree, then the whole right subtree. Recursing left first lets preIdxmarch through every left node before it reaches the right subtree's root. Reversing these two lines breaks the tree.
Complexity → O(n) time — each node is created once and its split is an O(1) map lookup. O(n) space for the index map plus the recursion stack (O(h) depth, up to O(n) for a skewed tree).

INTERVIEWFollow-ups they'll ask

  • "What if values repeat?"The map breaks — a value could appear at multiple inorder indices, so the split is ambiguous. The problem guarantees uniqueness; with duplicates you generally can't reconstruct a unique tree.
  • "Postorder + inorder instead?" Read postorder back-to-front (last element is the root) and recurse right before left. Same partition trick.
  • "Can you avoid the map?" Yes, but a linear search per node makes it O(n²). You can also pass an inorder boundary value and stop when you hit it, threading both pointers.
  • "Why not slice the arrays?" Slicing creates O(n) copies per call → O(n²) time and space. Index ranges + a shared preIdx keep it O(n).
  • "Iterative version?" Possible with an explicit stack tracking ancestors, comparing the next preorder value against the inorder pointer to decide left-child vs climbing for a right-child — trickier to get right than the recursion.

MNEMONIC The one-liner

"Preorder picks the root; inorder splits it. Left first, then right."

TRIGGERS When you see ___ → reach for ___

given preorder + inorder of a treeroot = preorder front, split inorder
"find root inside inorder fast"Map<value, inorderIndex>
no array slicing wantedshared preIdx pointer + [lo,hi] ranges
postorder + inorder variantroot from back, recurse right-then-left

SKELETON The reusable shape

skeleton.ts
const pos = new Map<number, number>();
inorder.forEach((v, i) => pos.set(v, i));
let preIdx = 0;
function build(lo: number, hi: number): TreeNode | null {
  if (lo > hi) return null;
  const root = new TreeNode(preorder[preIdx++]);
  const mid = pos.get(root.val)!;
  root.left = build(lo, mid - 1);
  root.right = build(mid + 1, hi);
  return root;
}
return build(0, inorder.length - 1);

FLASHCARDS Tap to flip

Which traversal gives you the roots, in order?
preorder— its first element is always the current subtree's root.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
In any preorder slice, which element is the subtree root?
QUESTION 02
What role does the inorder traversal play?
QUESTION 03
Why build a Map<value, inorderIndex> up front?
QUESTION 04
Why must the left subtree be built before the right?
QUESTION 05
What is the recursion base case?
QUESTION 06
Optimal time complexity with the index map?
QUESTION 07
Why avoid slicing the arrays (e.g. preorder.slice(1)) on each call?