DSA sheet · Trees · Construction pattern

Build Tree from Preorder & Inorder

This video starts the construction pattern: instead of reading a tree that already exists, we are handed two lists of numbers and we have to rebuild the tree from them. The teacher shows that preorder tells us who the root is, and inorder tells us which nodes go left and which go right. Put those two facts together, repeat them for every smaller piece, and the whole tree falls out. The same idea is reused in the next two problems (inorder + postorder, preorder + postorder), so it is worth learning this one very well.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

The node we will create

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val       # the number in this node
        self.left = left     # left child, or None
        self.right = right   # right child, or None

In this problem we don't get any nodes. We get plain numbers, and we make every node with TreeNode(value).

The three depth-first orders

A traversal is the order in which we visit the nodes. The three DFS orders differ only in when the root is written down:

nameorderwhere the root sits in the list
preorderroot → left subtree → right subtreefirst
inorderleft subtree → root → right subtreesomewhere in the middle (we can't tell where just by looking)
postorderleft subtree → right subtree → rootlast

A subtree is a node together with everything below it. "The left subtree of 3" means 3's left child and all of that child's descendants.

The example tree used all through this page:

        3
       / \
      9   20
     /   /  \
    4   15   7

preorder = [3, 9, 4, 20, 15, 7]     (root, then all of left, then all of right)
inorder  = [4, 9, 3, 15, 20, 7]     (all of left, then root, then all of right)

Why do we need TWO lists?

One list alone is not enough. Preorder [1, 2] could be either of these trees, because both write 1 first and then 2:

2 on the left
   1
  /
 2
2 on the right
   1
    \
     2

Their inorders are different though: [2, 1] for the first, [1, 2] for the second. So preorder gives us the root, and inorder settles the side. That is the whole reason the question gives us both.

Index ranges instead of new lists

We will often say "only look at the inorder list from index left to index right". That pair of numbers marks a window of the list. Every recursive call gets its own window, and the window is the set of nodes that call is allowed to use. When left > right, the window holds nothing.

Part A · Brute force: search the inorder list every time

LeetCode 105 · Construct Binary Tree from Preorder and Inorder Traversal

The teacher builds the full logic first and only then asks "how do we find the root inside inorder quickly?". Searching with a loop is the first idea, so we learn the whole recursion with that version. Part B then swaps the loop for a hashmap.

1The question in simple words

You get two lists, preorder and inorder, both made from the same binary tree. Build that tree and return its root node.

input:  preorder = [3, 9, 4, 20, 15, 7]
        inorder  = [4, 9, 3, 15, 20, 7]

output: the root of        3
                          / \
                         9   20
                        /   /  \
                       4   15   7

2What the constraints tell us

3Intuition: how to think about it

Think of the inorder list as a row of people standing left to right in the same left-to-right order as the tree. If you point at the root person, everybody standing to the root's left belongs to the left subtree, and everybody to the right belongs to the right subtree.

The only problem is that inorder doesn't tell us who the root is. That's where preorder comes in: preorder always writes the root first. So:

  1. Preorder says "the root is 3".
  2. Find 3 in inorder. Everything before it (4, 9) is the left subtree. Everything after it (15, 20, 7) is the right subtree.
  3. Now solve the same puzzle again for the left group, and again for the right group.
inorder:  [ 4   9 | 3 | 15  20   7 ]
            -----   ^   ----------
            left   root    right

4Building the logic from the example

Step 1 · The root comes from preorder, not inorder

Inorder is "left, root, right", so the root sits somewhere in the middle and we can't say where. Preorder is "root, left, right", so its very first value is definitely the root. In our example that's 3.

Step 2 · Find the root inside inorder to split the nodes

3 is at index 2 in inorder. Since inorder writes the whole left subtree before the root, indices 0 and 1 (values 4 and 9) form the left subtree. We don't know yet how 4 and 9 are arranged, only that they're on the left. Indices 3 to 5 (15, 20, 7) form the right subtree.

Doubt 1: why must the values be unique for this to work?
→ We find the root by its value. Suppose 3 appeared twice in inorder. The search would stop at the first 3, which might be the wrong one, and every split after that would be off. Worse, two different trees can then produce exactly the same lists. A root 3 with a left child 3, and a root 3 with a right child 3, both give preorder [3, 3] and inorder [3, 3], so nobody could tell which tree you meant. The constraints promise unique values, so this never happens.

Step 3 · Who is the root of the left group?

Look at preorder again: [3, 9, 4, 20, 15, 7]. After 3, preorder writes the whole left subtree before anything from the right. So the next value, 9, must be the root of the left subtree.

Now repeat step 2 for 9: find 9 in inorder (index 1). Whatever is before 9 is 9's left side, whatever is after is 9's right side.

Doubt 2: after 9 in inorder come 3, 15, 20 and 7. Are those all 9's right side?
→ No! 9 lives inside the left group of 3, which was only indices 0 to 1. The right side of 9 must stop before 3. This is why every recursive call needs to be told its window: "you may only use inorder from index left to index right". For 9 the window is 0..1, so 9's right side is index 2..1, which is empty. When we went into 3's left side we were allowed to move anywhere, but only from index 0 up to the index just before 3.

Step 4 · One moving pointer on preorder

Preorder is used in a simple way: take the next value, make it a node, move on. The first call takes 3, the next call takes 9, the next takes 4, then 20, 15, 7. So we keep one counter, pre_idx, that starts at 0 and goes up by one every time a node is made.

This works only if we always build the left subtree before the right subtree. Preorder lists the whole left subtree before the right subtree, so the counter reaches the left nodes first. The teacher puts it as: make the root first, then root.left, then root.right.

Doubt 3: in Python, why do I keep the counter as self.pre_idx and not as a normal variable or a parameter?
→ All the calls must share one counter. When the left call has used up 9 and 4, the right call must start at 20. If each call got its own copy (a parameter, or a local variable), the right call wouldn't know that the left call moved ahead. Storing it on self means every call reads and updates the same number. (In Java the teacher uses a class-level variable for the same reason.)

Step 5 · Where does the recursion stop?

In most tree problems we stop when the node is None. Here there are no nodes yet; we are making them. We are only moving index numbers around, so the stopping rule must be about the indexes.

Base caseif left > right: return None
Why None? The caller is waiting for a node to put into root.left or root.right. "No node here" is written as None.

Step 6 · Return the root so the parent can attach it

Each call makes one node, then fills its left and right by calling itself. When both children are done, it must return its node so the call above can store it. For example, the call for 4 gets None and None for its children, then returns 4. The call for 9 stores that 4 in 9.left, gets None on the right, and returns 9 to the call for 3, which stores it in 3.left.

The teacher's point: nodes are created on the way down, but they are linked together on the way back up, when each call returns its root.

Step 7 · Finding the root's position: the brute-force way

The simplest way to find the root inside inorder: walk the window from left to right until you see the value.

5Approach steps

  1. Set a shared counter pre_idx = 0.
  2. Call build(left=0, right=n-1).
  3. In build: if left > right, return None.
  4. Make a node from preorder[pre_idx], then add 1 to pre_idx.
  5. Loop over inorder from left to right to find the index mid of that value.
  6. Left child = build(left, mid - 1). Right child = build(mid + 1, right). Left first.
  7. Return the node.

6Code (Python)

Brute force: linear search in inorder
class Solution:
    def buildTree(self, preorder, inorder):
        self.pre_idx = 0                         # shared pointer into preorder
        return self.build(preorder, inorder, 0, len(inorder) - 1)

    def build(self, preorder, inorder, left, right):
        if left > right:                         # empty window
            return None
        root = TreeNode(preorder[self.pre_idx])  # next preorder value is the root
        self.pre_idx += 1

        mid = left                               # search the window for root.val
        while inorder[mid] != root.val:
            mid += 1

        root.left = self.build(preorder, inorder, left, mid - 1)
        root.right = self.build(preorder, inorder, mid + 1, right)
        return root

7Code line by line

linewhat it means
self.pre_idx = 0One counter for the whole build. It points at the next preorder value to turn into a node.
self.build(preorder, inorder, 0, len(inorder) - 1)The first call may use the whole inorder list, index 0 to the last index.
if left > right: return NoneThe window is empty, so there's no node here. This is what stops the recursion.
root = TreeNode(preorder[self.pre_idx]) self.pre_idx += 1Take the next preorder value as this window's root, then move the counter so the next call takes the next value.
mid = left while inorder[mid] != root.val: mid += 1Walk the window until we find the root. Because values are unique and the root is surely inside this window, the loop always stops.
root.left = self.build(..., left, mid - 1)Everything before the root in the window is the left subtree. mid itself is not included: it's the root.
root.right = self.build(..., mid + 1, right)Everything after the root is the right subtree. Runs after the left call has used up all the left nodes in preorder.
return rootHand this finished node back to the parent call, which attaches it.

8Dry run: count the searching work

The building steps are exactly the same as Part B (see the full dry run there). Here we only look at how many inorder cells each search reads, because that's the part the hashmap will remove.

node madewindow (left..right)cells read while searching
30..54, 9, 3 → 3 reads
90..14, 9 → 2 reads
40..04 → 1 read
203..515, 20 → 2 reads
153..315 → 1 read
75..57 → 1 read

Small here. But picture a tree that is a straight line going left (each node only has a left child). Preorder is [5, 4, 3, 2, 1], inorder is [1, 2, 3, 4, 5]. The root 5 is at the end of every window, so the searches read 5 + 4 + 3 + 2 + 1 cells. For n nodes that's about n²/2.

9Complexity & remember

Remember the recursionRoot = next preorder value. Split inorder at the root. Build left first, then right. Stop when left > right. Return the root.

Part B · Optimal: a hashmap for inorder positions

The question, the constraints, the intuition, the base case and the order of the calls are all the same as Part A. Only step 7 changes: how we find the root's index in inorder.

1The question in simple words

Same as Part A: rebuild the tree from preorder and inorder, but now in O(n) time.

2What the constraints tell us

3Intuition: what's wasteful in Part A?

For 3 we scanned to find it. For 9 we scanned again. For 4 again, for 20 again… For every one of the n nodes we walk the list. But the inorder list never changes! So we can walk it once at the start and write down where every value lives, like a phone book. After that, "where is 20?" takes one look-up, O(1).

The data structure that answers "where is this value?" in O(1) is a hashmap (a Python dict).

inorder = [4, 9, 3, 15, 20, 7]
index:     0  1  2   3   4  5

pos = { 4: 0,  9: 1,  3: 2,  15: 3,  20: 4,  7: 5 }

4Building the logic: what changes

Doubt 1: the map gives the index in the whole inorder list, not inside the window. Is that a problem?
→ No. The windows are also measured in whole-list indexes (0..1 for 3's left, 3..5 for 3's right, and so on). Since the root of a window is always inside that window, pos[root.val] lands between left and right every time.
Doubt 2: why not cut the lists into new smaller lists (slicing) instead of passing left and right?
→ Slicing copies elements, which costs O(size of the slice) at every call, and that brings us back towards O(n²). Two integers cost nothing to pass. So we keep one list and move the window.
Doubt 3: my machine says RecursionError on a 3000-node tree. Is the code wrong?
→ No. If the tree is a straight line, the calls go 3000 deep, and plain Python's default limit is about 1000. Add import sys; sys.setrecursionlimit(10000) when testing on your own computer. The logic doesn't change.

5Approach steps

  1. Build pos: for each index i in inorder, pos[inorder[i]] = i.
  2. Set pre_idx = 0 and call build(0, n - 1).
  3. In build: if left > right → return None.
  4. Make the node from preorder[pre_idx]; pre_idx += 1.
  5. mid = pos[node value].
  6. root.left = build(left, mid - 1), then root.right = build(mid + 1, right).
  7. Return root.

6Code (Python)

Optimal: hashmap of inorder positions
class Solution:
    def buildTree(self, preorder, inorder):
        self.pos = {}                            # value -> index in inorder
        for i in range(len(inorder)):
            self.pos[inorder[i]] = i
        self.pre_idx = 0                         # shared pointer into preorder
        return self.build(preorder, 0, len(inorder) - 1)

    def build(self, preorder, left, right):
        if left > right:                         # window crossed: no node
            return None
        root = TreeNode(preorder[self.pre_idx])
        self.pre_idx += 1

        mid = self.pos[root.val]                 # O(1) instead of a scan
        root.left = self.build(preorder, left, mid - 1)
        root.right = self.build(preorder, mid + 1, right)
        return root

7Code line by line

linewhat it means
self.pos = {} for i in range(len(inorder)): self.pos[inorder[i]] = iWalk inorder once and remember where every value sits. This is the only time we read inorder.
self.pre_idx = 0The next preorder value to use. Shared by every call.
self.build(preorder, 0, len(inorder) - 1)The top call may use the whole list. Inorder isn't passed: the map replaces it.
if left > right: return NoneNo nodes in this window → no child here.
root = TreeNode(preorder[self.pre_idx]) self.pre_idx += 1Make this window's root and move the pointer forward.
mid = self.pos[root.val]Jump straight to the root's place in inorder.
root.left = self.build(preorder, left, mid - 1)Left part of the window → left subtree. Must run first, because preorder lists the left subtree first.
root.right = self.build(preorder, mid + 1, right)Right part of the window → right subtree.
return rootGive the finished subtree to the parent. This is where the tree gets linked together.

8Dry run: watch the windows split

preorder = [3, 9, 4, 20, 15, 7]          pos = { 4:0, 9:1, 3:2, 15:3, 20:4, 7:5 }
inorder  = [4, 9, 3, 15, 20, 7]

In the boxes below, the yellow cell is the root of the window, plain cells are inside the window, and grey cells are outside it (this call may not touch them).

call 149315207build(0, 5): pre_idx 0 → 3; pos[3] = 2 → left (0, 1), right (3, 5)
call 249315207build(0, 1): pre_idx 1 → 9; pos[9] = 1 → left (0, 0), right (2, 1)
call 349315207build(0, 0): pre_idx 2 → 4; pos[4] = 0 → left (0, −1), right (1, 0)
calls 4, 5build(0, −1) and build(1, 0): left > right → None, None. 4 is a leaf → return 4 → 9.left = 4
call 6back in 9: build(2, 1) → 2 > 1 → None. 9 has no right child. Return 9 → 3.left = 9
call 749315207build(3, 5): pre_idx 3 → 20; pos[20] = 4 → left (3, 3), right (5, 5)
call 849315207build(3, 3): pre_idx 4 → 15; children (3, 2), (4, 3) → None, None → return 15 → 20.left = 15
call 949315207build(5, 5): pre_idx 5 → 7; children (5, 4), (6, 5) → None, None → return 7 → 20.right = 7
end20 returns → 3.right = 20. Call 1 returns 3, the root of the finished tree. pre_idx = 6 = n, every value used once ✓

Notice how pre_idx moved 0, 1, 2, 3, 4, 5 in order, and the nodes were made in the order 3, 9, 4, 20, 15, 7, exactly the preorder list. That only happens because the left call always runs before the right call.

stack at call 3 (deepest on the left)
build(0,5) node 3build(0,1) node 9build(0,0) node 4
stack at call 8
build(0,5) node 3build(3,5) node 20build(3,3) node 15
after the end
empty → return 3

Newest call on top (red). When the call for 4 leaves the stack, 4 gets linked under 9. When 9 leaves, 9 gets linked under 3. The tree is joined from the bottom up.

result:     3
           / \
          9   20
         /   /  \
        4   15   7

9Complexity & remember

RememberMap inorder value → index once. Shared pre_idx from 0 going up. build(left, mid-1) then build(mid+1, right). left > right → None.

Part C · Revision page

Brute force (Part A)Optimal (Part B)
root comes frompreorder, using a shared pointer that goes 0, 1, 2, …
split comes frominorder: left of the root's index = left subtree, right of it = right subtree
finding the root in inorderscan the window: O(n) per nodepos[val]: O(1) per node
arguments of buildpreorder, inorder, left, rightpreorder, left, right (map replaces inorder)
base caseleft > right → None
call orderleft first, then right (preorder lists the left subtree first)
timeO(n²) (9 × 10⁶ at n = 3000, still passes)O(n)
spaceO(h) stackO(n) map + O(h) stack = O(n)
traversalwhat it gives us here
preorder (root, left, right)the root of each window: the next unused value
inorder (left, root, right)the split: which nodes are left and which are right of that root
If you remember only 5 lines 1. Preorder's next value is the root; inorder splits around it.
2. Every call gets a window left..right of inorder and builds only from it.
3. left > right means an empty window → return None.
4. Store inorder positions in a dict once → O(n) total.
5. Build the left child before the right child, and return the root so the parent can link it.
Mistakes to avoid ✗ taking the root from inorder (its root is hidden in the middle)
✗ passing pre_idx as a parameter (each call gets its own copy, so the count breaks)
✗ building the right child first (the preorder pointer would hand left nodes to the right side)
✗ using mid instead of mid - 1 / mid + 1 (the root would be used twice)
✗ forgetting return root (nothing gets linked)
✗ slicing lists in every call (hidden O(n) copies)
test it yourself (paste under either solution above)
def preorder_of(node):
    return [node.val] + preorder_of(node.left) + preorder_of(node.right) if node else []

def inorder_of(node):
    return inorder_of(node.left) + [node.val] + inorder_of(node.right) if node else []

pre = [3, 9, 4, 20, 15, 7]
ino = [4, 9, 3, 15, 20, 7]
root = Solution().buildTree(pre, ino)
print(root.val, root.left.val, root.right.val)              # 3 9 20
print(preorder_of(root) == pre, inorder_of(root) == ino)    # True True
print(Solution().buildTree([1], [1]).val)                   # 1

Based on this video: Construct Binary Tree from Preorder and Inorder Traversal