DSA sheet · Trees · Construction pattern

Build Tree from Preorder & Postorder

The third construction problem, and the teacher admits it is a little harder than the two before it (preorder + inorder and inorder + postorder). This time there is no inorder list, so nothing tells us directly where the left subtree ends. The trick she teaches: the node right after the root in preorder is the left subtree's root, and its position in postorder tells us how big the left subtree is. With that size we can cut both lists into "left part" and "right part". Because we must cut two lists, we track four indexes instead of two.

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

Recap of the orders we're given

nameorderwhere the root sits
preorderroot → left subtree → right subtreefirst
postorderleft subtree → right subtree → rootlast

A subtree is a node with everything below it. The size of a subtree is how many nodes it has. A leaf is a node with no children.

The example tree

          1
        /   \
       2     3
      / \   / \
     4   5 6   7

index:        0  1  2  3  4  5  6
preorder  = [ 1, 2, 4, 5, 3, 6, 7 ]
postorder = [ 4, 5, 2, 6, 7, 3, 1 ]

What's different from the last two problems?

Both preorder and postorder tell us who the root is (first of preorder, last of postorder). Neither of them, alone, tells us where the left subtree stops and the right subtree starts. Inorder did that job before. Now we have to work out that boundary ourselves.

Doubt: the problem says "if there are multiple answers, return any of them". How can there be more than one answer?
→ When a node has only one child, preorder and postorder can't tell whether that child is on the left or the right. Both trees below have preorder [1, 2] and postorder [2, 1]:
   1          1
  /            \
 2              2
Inorder would tell them apart, but we don't have it. So the judge accepts either. Our method will always put a lone child on the left. If every node has 0 or 2 children (a "full" binary tree), the answer is unique.

Part A · Build the tree with four index pointers

LeetCode 889 · Construct Binary Tree from Preorder and Postorder Traversal

1The question in simple words

You get preorder and postorder of one binary tree (not a binary search tree, just a binary tree). Rebuild it and return its root. If more than one tree fits, any of them is accepted.

input:  preorder  = [1, 2, 4, 5, 3, 6, 7]
        postorder = [4, 5, 2, 6, 7, 3, 1]

output: the root of        1
                         /   \
                        2     3
                       / \   / \
                      4   5 6   7

2What the constraints tell us

3Intuition: how to think about it

Look at preorder: [1 | 2, 4, 5 | 3, 6, 7]. The root 1 comes first, then the whole left subtree, then the whole right subtree. If only we knew that the left subtree has 3 nodes, we could cut preorder right there.

Who is the left subtree's root? In preorder, the node right after the root, so 2. Now find 2 in postorder: [4, 5, 2 | 6, 7, 3 | 1]. Postorder writes the root of a subtree after all its other nodes, so 2 is the last node of the left subtree there. Everything from the start up to 2 is the left subtree: 3 nodes.

preorder  = [ 1 | 2  4  5 | 3  6  7 ]        left root = 2 (next after the root)
              ^   -------   -------
             root   left     right

postorder = [ 4  5  2 | 6  7  3 | 1 ]        2 sits at index 2 → left size = 3
              -------   -------   ^
               left      right   root

So the picture is: preorder gives the left root, postorder gives the left size, and the size cuts both lists. Then do the same for each half.

4Building the logic from the example

Step 1 · The root

The root of the window is the first value of its preorder part: preorder[pre_start]. (For the full tree it's also the last of postorder, but we read it from preorder.) The teacher first writes index 0, then notices the next call needs a different start (2 will be the root of the next call), so she uses a variable pre_start instead.

Step 2 · The left root and its index in postorder

left_root = preorder[pre_start + 1] = 2. To find where 2 sits in postorder in O(1), we fill a hashmap once at the start: pos[value] = index in postorder. pos[2] = 2.

Doubt 1: why a hashmap? I could just loop over postorder to find 2.
→ The whole build already takes O(n) (one step per node). A loop inside every step adds another O(n), making it O(n²). Values are unique, so a map of value → index works and makes each look-up O(1). This is the same idea as the previous two problems.

Step 3 · The size of the left subtree

Size formulasize = idx - post_start + 1

This is the usual "right − left + 1" count of how many cells lie between two indexes, using the postorder window: it starts at post_start and the left subtree ends at idx.

Doubt 2: for the top call post_start is 0, so why not just idx + 1?
→ Because in deeper calls the window doesn't start at 0. Take the right subtree of 1: its postorder window is 3..5 (6, 7, 3). Its left root is 6, and pos[6] = 3. The correct size is 3 − 3 + 1 = 1. With idx + 1 you'd get 4, which is more nodes than the whole window holds. You must subtract where the current window starts.

Step 4 · The four indexes for the left call

The teacher compares two choices. Either slice the lists and send smaller copies, or send the same full lists every time with indexes that mark the allowed part. Slicing costs extra O(n) work at every call, so she goes with indexes: pre_start, pre_end for preorder and post_start, post_end for postorder.

startendexample (top call)
left: preorderpre_start + 1 (skip the root)pre_start + size1..3 → 2, 4, 5
left: postorderpost_startidx (the left root itself)0..2 → 4, 5, 2

On screen she first said the left post end would be "size", then corrected herself to idx. In the top call they happen to be close (size 3, idx 2), but idx is the right one: the left subtree ends exactly where its root sits in postorder.

Step 5 · The four indexes for the right call

startendexample (top call)
right: preorderpre_start + size + 1pre_end4..6 → 3, 6, 7
right: postorderidx + 1post_end - 13..5 → 6, 7, 3
Doubt 3: why post_end - 1 and not post_end?
→ The last cell of the postorder window is the current root (1 here, at index 6). It belongs to neither subtree, so the right part stops one cell before it. On the preorder side the root was the first cell, which is why there we skipped it with + 1 at the start.

Step 6 · The base cases (there are two)

(a) Empty window. When the start passes the end, there are no nodes → return None, because the caller is waiting for a node to attach and "no node" is None.

Base case 1if pre_start > pre_end: return None
Doubt 4: why only check the preorder pair? What about post_start > post_end?
→ The two windows always hold the same number of nodes. Left: preorder pre_start+1 .. pre_start+size has size cells, postorder post_start .. idx also has size cells. Right: both get "everything else except the root". So if one window is empty, the other is too. Checking either pair is enough.

(b) Exactly one node. When pre_start == pre_end, the window holds one node, a leaf. The teacher asks: should we still go left and right? No. Make the node and return it straight away.

Base case 2if pre_start == pre_end: return root (right after making the root)
Doubt 5: is the leaf check just a shortcut, or is it needed?
→ It's needed. Without it, a leaf would still read preorder[pre_start + 1] as its "left root", which is a node from outside its window. For the leaf 4 (window 2..2) that's 5, so 4 would wrongly get 5 below it. On the example, the windows keep going wrong after that until a read lands past the end of the list (index 7 of a 7-cell list), and Python crashes with IndexError. The previous two problems never needed this check, because they never peeked at a second element.

Step 7 · Return the root so it gets linked

The empty-window return None covers missing children. But a real node must also be handed back: after 4 gets None and None, it returns 4 so it lands in 2.left; 2 returns itself to land in 1.left. Nodes are made going down, and joined into one tree as each call returns.

Why not just slice? (the version she rejects)

For comparison, here is the slicing idea written out. It's shorter to read, and with n ≤ 30 it would pass, but every slice and every .index() walks part of a list, so each call does O(n) extra work: O(n²) overall.

Slicing version (works, but O(n²))
class Solution:
    def constructFromPrePost(self, preorder, postorder):
        if not preorder:                           # empty part
            return None
        root = TreeNode(preorder[0])
        if len(preorder) == 1:                     # a leaf
            return root
        idx = postorder.index(preorder[1])         # O(n) search
        size = idx + 1                             # here every slice starts at 0
        root.left = self.constructFromPrePost(preorder[1:size + 1], postorder[:size])
        root.right = self.constructFromPrePost(preorder[size + 1:], postorder[size:-1])
        return root

Note that here size = idx + 1 is correct, because each slice is a fresh list starting at index 0. In the index version the window does not start at 0, which is exactly why Step 3 subtracts post_start.

5Approach steps

  1. Fill pos[postorder[i]] = i for every i.
  2. Call build(0, n-1, 0, n-1) (preorder window, postorder window).
  3. If pre_start > pre_end → return None.
  4. Make root from preorder[pre_start].
  5. If pre_start == pre_end → it's a leaf, return root.
  6. left_root = preorder[pre_start + 1], idx = pos[left_root], size = idx - post_start + 1.
  7. Left: preorder pre_start+1 .. pre_start+size, postorder post_start .. idx.
  8. Right: preorder pre_start+size+1 .. pre_end, postorder idx+1 .. post_end-1.
  9. Return root.

6Code (Python)

Preorder + Postorder with four pointers
class Solution:
    def constructFromPrePost(self, preorder, postorder):
        self.pos = {}                                # value -> index in postorder
        for i in range(len(postorder)):
            self.pos[postorder[i]] = i
        n = len(preorder)
        return self.build(preorder, 0, n - 1, postorder, 0, n - 1)

    def build(self, preorder, pre_start, pre_end, postorder, post_start, post_end):
        if pre_start > pre_end:                      # empty window
            return None
        root = TreeNode(preorder[pre_start])
        if pre_start == pre_end:                     # one node: a leaf
            return root

        left_root = preorder[pre_start + 1]          # root of the left subtree
        idx = self.pos[left_root]                    # where it sits in postorder
        size = idx - post_start + 1                  # nodes in the left subtree

        root.left = self.build(preorder, pre_start + 1, pre_start + size,
                               postorder, post_start, idx)
        root.right = self.build(preorder, pre_start + size + 1, pre_end,
                                postorder, idx + 1, post_end - 1)
        return root

7Code line by line

linewhat it means
self.pos[postorder[i]] = iRemember where each value sits in postorder (not inorder this time; we don't have one).
self.build(preorder, 0, n - 1, postorder, 0, n - 1)Top call: both windows are the whole lists.
if pre_start > pre_end: return NoneNo nodes in the window → no child here.
root = TreeNode(preorder[pre_start])The first cell of the preorder window is this window's root.
if pre_start == pre_end: return rootOnly one node → a leaf. Return now, before we peek at a next element that isn't ours.
left_root = preorder[pre_start + 1]In preorder the left subtree starts right after the root, so this is its root.
idx = self.pos[left_root]In postorder a subtree's root is its last node, so idx is where the left subtree ends.
size = idx - post_start + 1How many nodes from the window's start up to idx: the left subtree's size.
root.left = self.build(..., pre_start + 1, pre_start + size, ..., post_start, idx)Left subtree: skip the root in preorder and take size cells; in postorder take from the start up to idx.
root.right = self.build(..., pre_start + size + 1, pre_end, ..., idx + 1, post_end - 1)Right subtree: whatever is left in preorder; in postorder everything after idx except the root at the end.
return rootHand the finished subtree to the parent so it gets attached.

8Dry run: watch all four indexes

index:        0  1  2  3  4  5  6
preorder  = [ 1, 2, 4, 5, 3, 6, 7 ]
postorder = [ 4, 5, 2, 6, 7, 3, 1 ]
pos = { 4:0, 5:1, 2:2, 6:3, 7:4, 3:5, 1:6 }

Each call is written as (pre_start, pre_end | post_start, post_end), the same way the teacher writes them on the board. Top row of boxes = preorder, bottom row = postorder. Yellow = this call's root, plain = inside the window, grey = outside.

call 1(0, 6 | 0, 6) → root 1; left root pre[1] = 2; idx = pos[2] = 2; size = 2 − 0 + 1 = 3
pre1245367
post4526731→ left (1, 0+3 | 0, 2) = (1, 3 | 0, 2); right (0+3+1, 6 | 2+1, 6−1) = (4, 6 | 3, 5)
call 2(1, 3 | 0, 2) → root 2; left root pre[2] = 4; idx = pos[4] = 0; size = 0 − 0 + 1 = 1
pre1245367
post4526731→ left (2, 2 | 0, 0); right (1+1+1, 3 | 0+1, 2−1) = (3, 3 | 1, 1)
call 31245367(2, 2 | 0, 0) → root 4; start == end → leaf → return 4 → 2.left = 4
call 41245367(3, 3 | 1, 1) → root 5; leaf → return 5 → 2.right = 5. Call 2 returns 2 → 1.left = 2
call 5(4, 6 | 3, 5) → root 3; left root pre[5] = 6; idx = pos[6] = 3; size = 3 − 3 + 1 = 1
pre1245367
post4526731→ left (5, 4+1 | 3, 3) = (5, 5 | 3, 3); right (4+1+1, 6 | 3+1, 5−1) = (6, 6 | 4, 4)
call 61245367(5, 5 | 3, 3) → root 6; leaf → 3.left = 6
call 71245367(6, 6 | 4, 4) → root 7; leaf → 3.right = 7. Call 5 returns 3 → 1.right = 3
endcall 1 returns 1 to the main function; the stack is empty ✓

Look at call 5: its postorder window starts at 3, not 0. That's the case from Doubt 2, and subtracting post_start is what gives the right size of 1.

stack at call 3
(0,6|0,6) node 1(1,3|0,2) node 2(2,2|0,0) node 4
stack at call 4 (call 3 has left)
(0,6|0,6) node 1(1,3|0,2) node 2(3,3|1,1) node 5
stack at call 6
(0,6|0,6) node 1(4,6|3,5) node 3(5,5|3,3) node 6

The teacher's point about space: the call for 5 only enters the stack after the call for 4 has left it, so at any moment the stack holds about one call per level.

9Complexity & remember

RememberRoot = pre[pre_start]. Leaf if pre_start == pre_end. Left root = pre[pre_start+1], find it in post → size = idx - post_start + 1. Left: (ps+1, ps+size | post_s, idx). Right: (ps+size+1, pe | idx+1, post_e-1).

Part B · Revision page

Pre + In (105)In + Post (106)Pre + Post (889)
root frompreorder, pointer going uppostorder, pointer going downpreorder[pre_start]
hashmap oninorderinorderpostorder
how we splitroot's index in inorderroot's index in inorderleft root's index in postorder → size
indexes tracked2 (left, right) + shared pointer2 + shared pointer4: pre_start, pre_end, post_start, post_end
base casesleft > rightleft > rightpre_start > pre_end and leaf check
unique answer?yesyesonly for full trees; otherwise any valid tree
time / spaceO(n) / O(n) (map) + O(h) stack
child callpreorder windowpostorder window
leftpre_start + 1 .. pre_start + sizepost_start .. idx
rightpre_start + size + 1 .. pre_endidx + 1 .. post_end - 1
If you remember only 5 lines 1. Preorder's first cell is the root; the next cell is the left subtree's root.
2. Find that left root in postorder: it's the last node of the left subtree.
3. size = idx - post_start + 1 cuts both lists.
4. Two base cases: empty window → None; one node → return it right away.
5. Use indexes, not slices (slicing adds O(n) per call).
Mistakes to avoid ✗ forgetting the leaf check (reads a node outside the window, or crashes at the end)
✗ size = idx + 1 in the index version (wrong in every window not starting at 0)
✗ using post_end instead of post_end - 1 for the right call (the root sneaks back in)
✗ putting size instead of idx as the left post end
✗ expecting one exact tree when a node has a single child (any valid tree is accepted)
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 postorder_of(node):
    return postorder_of(node.left) + postorder_of(node.right) + [node.val] if node else []

pre = [1, 2, 4, 5, 3, 6, 7]
post = [4, 5, 2, 6, 7, 3, 1]
root = Solution().constructFromPrePost(pre, post)
print(root.val, root.left.val, root.right.val)                 # 1 2 3
print(preorder_of(root) == pre, postorder_of(root) == post)    # True True
one = Solution().constructFromPrePost([1, 2], [2, 1])
print(one.left.val, one.right)                                 # 2 None

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