Skip to content

Tree Construction from Traversals

Reconstruction problems all rest on the same two-part observation:

One traversal tells you the roots. Another tells you the split.

Preorder and postorder both reveal which element is a subtree’s root — preorder at the front, postorder at the back. Inorder reveals where the left subtree ends and the right begins, because everything before the root is left and everything after is right.

Put them together and the tree is determined. Each alone is not enough, and knowing why is the first question you will be asked.

  • Why a single traversal is ambiguous, and which pairs are sufficient.
  • The recursive template, and the index map that removes the O(n)O(n) search.
  • The shared index trick — why one moving pointer beats slicing arrays.
  • Why postorder must build the right subtree first.
  • Three real LeetCode problems solved in the browser: 105, 106, 654.

The two arrays the problem hands you are the outputs of these two walks over the tree you have to rebuild. Reading them side by side is what the algorithm does.

Pre-order — the walk that tells you which node is the root of every window:

treePre-order emits [3, 9, 20, 15, 7] — every element is a root, in the order you need themroot, left, right
9315207
call stack
3
node3stack depth1
enterEnter 3. The recursive call is pushed onto the stack, which is now 1 frame deep.
1/16

Pre-order visits a node before either subtree, so its elements arrive in exactly the order the recursion wants to create nodes. That is why a single ever-advancing pointer into the pre-order array is enough -- it never has to move backwards.

In-order — the walk that tells you how big each subtree is:

treeIn-order emits [9, 3, 15, 20, 7] — the root splits it into left and rightleft, root, right
9315207
call stack
3
node3stack depth1
enterEnter 3. The recursive call is pushed onto the stack, which is now 1 frame deep.
1/16

Find 3 in the in-order array at position 1: everything before it (just 9) is the left subtree, everything after (15, 20, 7) is the right. Neither array alone is enough -- pre-order gives roots without sizes, in-order gives sizes without roots.

build_pre_in.py
def build_tree(preorder, inorder):
    index = {v: i for i, v in enumerate(inorder)}   # value -> inorder position
    pre = 0                                          # shared, ever-advancing pointer
 
    def helper(left, right):
        """Build from the inorder window [left, right]."""
        nonlocal pre
        if left > right:
            return None
 
        root_val = preorder[pre]      # preorder's next element is this root
        pre += 1
        node = TreeNode(root_val)
 
        mid = index[root_val]         # where the root sits in inorder
        node.left = helper(left, mid - 1)     # LEFT first -- matches preorder order
        node.right = helper(mid + 1, right)
        return node
 
    return helper(0, len(inorder) - 1)

Two design choices carry the complexity:

Postorder is left, right, root. Read backwards it is root, right, left. So if you consume postorder from the end, you must construct the right child before the left:

build_in_post.py
def build_tree(inorder, postorder):
    index = {v: i for i, v in enumerate(inorder)}
    post = len(postorder) - 1          # consume from the END
 
    def helper(left, right):
        nonlocal post
        if left > right:
            return None
        root_val = postorder[post]
        post -= 1
        node = TreeNode(root_val)
        mid = index[root_val]
        node.right = helper(mid + 1, right)   # RIGHT before LEFT
        node.left = helper(left, mid - 1)
        return node
 
    return helper(0, len(inorder) - 1)

Building left first here silently produces a mirrored, wrong tree — the code runs, returns a tree of the right size, and fails the tests. It is the defining mistake of LC 106.

LC 105 — preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]. The index map is {9:0, 3:1, 15:2, 20:3, 7:4}, and pre starts at 0. Each row is one helper call, indented by recursion depth.

depthin-order windowpreorder[pre]midpre afterresult
0[0,4]311root 3; left gets [0,0], right gets [2,4]
1[0,0]902node 9
2[0,-1]2left > rightNone
2[1,0]2None — 9 is a leaf
1[2,4]2033node 20; left [2,2], right [4,4]
2[2,2]1524node 15, both children empty
2[4,4]745node 7, both children empty

Built tree: [3, 9, 20, null, null, 15, 7]. ✓

Four things to take from that table:

  • pre only ever moves forward, and it advances exactly once per node — 5 nodes, pre ends at 5. The empty windows consume nothing. That is the invariant which makes a shared pointer correct: pre-order produces nodes in the same order the recursion creates them, so there is never a reason to look back.
  • The windows are in-order indices, not pre-order indices. helper(left, right) never indexes preorder by anything but pre. Mixing the two index spaces is the most common way this template goes wrong.
  • left > right is the base case, not left == right. Notice the two “windows” [0,-1] and [1,0]: both are empty, both are reached, and both would be out-of-bounds errors under any other test. A single-element window like [2,2] is not empty — it is a leaf.
  • The order of the two recursive calls is forced. node.left must be built first, because the next pre-order element belongs to the left subtree. Swap the two lines and the tree comes out mirrored — with the right size, so the code runs and the tests fail. That is the same trap, inverted, as post-order’s right-before-left.
ApproachTimeSpace
Index map + shared pointerO(n)O(n)O(n)O(n) for the map, O(h)O(h) recursion
inorder.index(root) per nodeO(n2)O(n^2)O(h)O(h)
Array slicing per callO(n2)O(n^2)O(n2)O(n^2) — every level copies
From a rule, e.g. LC 654 max treeO(n2)O(n^2) naive, O(n)O(n) with a monotonic stackO(n)O(n)

The map is the whole difference. inorder.index(v) is an O(n)O(n) scan per node, so a degenerate tree with n=3000n = 3000 costs about nine million comparisons instead of three thousand dictionary lookups. Slicing is worse still, because it also allocates: each of the hh levels copies O(n)O(n) elements.

Some problems define the tree by a property rather than a traversal. LC 654 builds a maximum binary tree: the root is the largest element, and the left and right subtrees are built recursively from the elements on either side.

The naive recursion finds the maximum in each range — O(n2)O(n^2). A monotonic stack does it in O(n)O(n):

max_binary_tree.py
def construct_maximum_binary_tree(nums):
    stack = []                        # values strictly decreasing
    for n in nums:
        node = TreeNode(n)
        while stack and stack[-1].val < n:
            node.left = stack.pop()   # everything smaller becomes our left subtree
        if stack:
            stack[-1].right = node    # we hang off the nearest larger element
        stack.append(node)
    return stack[0]                   # the bottom of the stack is the global max

Each node is pushed once and popped at most once, so it is O(n)O(n). The stack bottom ends up holding the largest value — the root.

ApproachTimeSpace
Slicing recursionO(n2)O(n^2)O(n2)O(n^2)
Index map + shared pointerO(n)O(n)O(n)O(n)
Find-the-max recursion (654)O(n2)O(n^2)O(n)O(n)
Monotonic stack (654)O(n)O(n)O(n)O(n)
VariantThe root comes fromThe split comes fromProblem
Preorder + inorderFront of preorderInorder position105
Inorder + postorderBack of postorderInorder position106
Preorder + postorderFront of preorderPostorder (full trees only)889
Preorder of a BSTFront of preorderSort it to get inorder, or use bounds1008
Sorted arrayThe middle elementArray halves108
Maximum ruleThe largest elementMonotonic stack654
Serialised stringThe token streamExplicit null markers297

LC 105 — Construct Binary Tree from Preorder and Inorder Traversal · Medium

Section titled “LC 105 — Construct Binary Tree from Preorder and Inorder Traversal · Medium”

Problem. Given preorder and inorder traversals of a binary tree with distinct values, construct and return the tree.

Constraints. 1 <= len(preorder) <= 3000, both traversals are of the same tree, all values distinct.

Examples. preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] gives the tree [3,9,20,null,null,15,7] · preorder = [-1], inorder = [-1] gives [-1]

Editorial — approach, complexity, follow-ups

Preorder gives the root; inorder tells you how many nodes fall in the left subtree. Recurse on the inorder window and let the preorder pointer advance naturally.

Time O(n)O(n). Space O(n)O(n) for the map plus O(h)O(h) recursion.

Why the shared pointer works: helper consumes exactly one preorder element per node created, and it creates nodes in preorder order (root, then the whole left subtree, then the whole right). So the pointer never needs to jump — it just walks forward.

[1,2,3] / [3,2,1] is a good check: preorder root 1, and inorder puts everything before it, so 1 has only a left child. The result is a left-leaning chain, [1, 2, None, 3] in level order.

Follow-ups you should expect:

  • “What if values could repeat?” The index map becomes ambiguous and the tree is not uniquely determined. You would need to pass explicit ranges and potentially backtrack.
  • “Preorder + postorder (LC 889)?” Insufficient in general — only unique for full binary trees (every node has 0 or 2 children), because without inorder there is no way to tell a lone child’s side.
  • “Iteratively?” Possible with an explicit stack: push nodes as you consume preorder, popping when the top matches the current inorder element. Harder to get right; know it exists.
  • “Do it without the index map?” O(n2)O(n^2); state the trade-off.

LC 106 — Construct Binary Tree from Inorder and Postorder Traversal · Medium

Section titled “LC 106 — Construct Binary Tree from Inorder and Postorder Traversal · Medium”

Problem. Given inorder and postorder traversals of a binary tree with distinct values, construct and return the tree.

Constraints. 1 <= len(inorder) <= 3000, both of the same tree, distinct values.

Examples. inorder = [9,3,15,20,7], postorder = [9,15,7,20,3] gives [3,9,20,null,null,15,7]

Editorial — approach, complexity, follow-ups

Identical to LC 105 with two changes: the root comes from the end of postorder, and the recursion builds right before left.

Time O(n)O(n). Space O(n)O(n).

The ordering is the whole lesson. Postorder is left, right, root; walking it backwards yields root, right, left. Since the pointer moves strictly backwards, the recursion must ask for the right subtree first — otherwise the pointer hands left-subtree values to the right subtree and you get a mirrored tree.

That failure mode is nasty precisely because it is plausible: the tree has the correct node count and a valid shape, so only a value-by-value comparison reveals it. If your LC 106 output looks like a reflection of the expected answer, this is the line to check.

[3,2,1] / [3,2,1] is worth tracing: postorder’s last element 1 is the root; inorder places 3 and 2 before it, so both are in the left subtree, giving the left-leaning chain [1, 2, None, 3].

Follow-ups you should expect: “Why right before left?” — the likeliest question; answer with the reversed-postorder observation. “Could you reverse postorder and reuse the LC 105 code?” — yes, with mirrored child assignment; some find that clearer. “Preorder + postorder?” — LC 889, full trees only. “Construct a BST from preorder alone (LC 1008)?” — sorting gives inorder, or better, recurse with (low, high) bounds in O(n)O(n).

Problem. Build a maximum binary tree from nums: the root is the maximum element; its left subtree is built from the elements to the left of that maximum, its right subtree from the elements to the right. Recurse.

Constraints. 1 <= len(nums) <= 1000, 0 <= nums[i] <= 1000, all values unique.

Examples. [3,2,1,6,0,5] gives [6,3,5,null,2,0,null,null,1] · [3,2,1] gives [3,null,2,null,1]

Editorial — approach, complexity, follow-ups

Start with the naive solution and say it: find the maximum in the range, make it the root, recurse on both halves. O(n2)O(n^2) worst case (a sorted array), and accepted at n <= 1000.

The O(n)O(n) solution uses a decreasing monotonic stack. The invariant: the stack holds the right spine of the tree built so far, values decreasing from bottom to top.

When a new value n arrives:

  • Every stacked value smaller than n must end up in n’s left subtree, because n is larger and appears later — so it becomes their ancestor. Popping them in order leaves the largest of them as n’s left child.
  • If a larger value remains on the stack, n sits to its right and is later, so n becomes that node’s right child (replacing whatever was there — correctly, since the replaced subtree has just been absorbed into n’s left).

Time O(n)O(n) — each node pushed once, popped at most once. Space O(n)O(n).

[1,2,3] giving [3,2,None,1] is a useful trace: each new value is larger, so each pops the previous and takes it as a left child, ending with root 3.

Follow-ups you should expect: “Why is the stack bottom the root?” — nothing ever pops it, because no later value exceeds the global maximum. “Minimum binary tree instead?” — flip the comparison to an increasing stack. “What if values repeated?” — the tree is no longer uniquely defined; you must specify a tie-break. “Maximum Binary Tree II (LC 998)?” — insert a value appended to the original array by walking the right spine, which is the same invariant.

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

5 problems
0 easy5 medium0 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

They askWhat they’re checkingThe answer
“Why isn’t one traversal enough?”FoundationsIt records roots but not sides — [1,2] preorder fits two different trees
“Which pairs are sufficient?”Precisionpre+in and post+in always; pre+post only for full trees; a BST’s preorder alone suffices
“Why the index map?”ComplexityIt replaces an O(n)O(n) scan per node, turning O(n2)O(n^2) into O(n)O(n); it needs distinct values
“Why not slice the arrays?”Hidden costsSlicing copies, giving O(n2)O(n^2) time and space
“Why right before left for postorder?”The key detailReversed postorder is root, right, left — building left first mirrors the tree
“What if values repeat?”LimitsThe map is ambiguous and the tree is not uniquely determined
“Can 654 be done in O(n)O(n)?”DepthYes — a decreasing monotonic stack
  • Single node — every template must handle n == 1.
  • Left-only chainpreorder [1,2,3], inorder [3,2,1]; also the O(n2)O(n^2) worst case for the slicing version.
  • Right-only chain — the mirror; catches the LC 106 ordering bug.
  • Negative values[-1]; do not assume non-negative anywhere.
  • Mismatched traversal lengths — the problems guarantee consistency, but say what you would validate.
  • Duplicate values — breaks the index map; know that reconstruction is not unique.
  • Sorted input to LC 654 — the naive recursion’s worst case, and where the stack version wins.
  • Deep recursion — a 3000-node chain is fine under Python’s 1000-frame limit only if you raise it; worth mentioning.
pch.quizTag Tree construction — self-check
  1. What does each of the two traversals contribute?

    pch.quizShowAnswer

    B — Pre-order tells you which element is the root of each window; in-order tells you how that window splits into left and right subtrees — Roots without sizes, and sizes without roots — neither array alone determines the tree. That is also why serialisation with null markers needs only one traversal: the markers supply the missing structure instead.

  2. Why is one shared, ever-advancing `pre` pointer correct?

    pch.quizShowAnswer

    B — Because pre-order produces nodes in exactly the order the recursion creates them — one element consumed per node, never a reason to look backwards — In the dry run, pre advances exactly 5 times for 5 nodes and the empty windows consume nothing. Recognising that the pointer never rewinds is what replaces array slicing.

  3. What does the index map `{value: inorder position}` buy, and what does it assume?

    pch.quizShowAnswer

    B — It turns an O(n) `inorder.index(root)` scan per node into an O(1) lookup, making the build O(n) instead of O(n²) — and it assumes distinct values — On a 3000-node degenerate tree that is ~9 million comparisons versus 3000 lookups. With duplicates the map is ambiguous — and reconstruction from two traversals is not unique either, so the problem itself becomes ill-posed.

  4. Why is the slicing version O(n²) in space as well as time?

    pch.quizShowAnswer

    B — Because every level of the recursion copies subarrays of the pre-order and in-order lists, so O(h) levels each allocate O(n) elements — The slicing version reads beautifully, which is why it is worth knowing why not to write it. It also makes the pre-order slice bounds easy to get wrong.

  5. For in-order + post-order (LC 106), why must the right child be built before the left?

    pch.quizShowAnswer

    B — Because post-order read backwards is root, right, left — so consuming from the end of the array yields the right subtree's root next — Building left first produces a mirrored tree of the correct size, so it runs and fails the tests silently. It is the defining mistake of LC 106.

  6. Why is the base case `left > right` rather than `left == right`?

    pch.quizShowAnswer

    B — Because a single-element window like [2,2] is a valid leaf, not an empty subtree — and empty windows legitimately arrive as [0,-1] or [1,0], which any other test would mishandle — Both out-of-order windows appear in the dry run. Treating left == right as empty would silently drop every leaf.

  • Cue — “construct the binary tree from these two traversals”, or “build the tree defined by this rule”.
  • What each array gives — pre-order (or reversed post-order): the root of each window. In-order: where that root splits the window.
  • Templateindex = {v: i for i, v in enumerate(inorder)}; one nonlocal pointer into the pre/post array; helper(left, right) over in-order indices.
  • Base caseleft > right. A single-element window is a leaf, not empty.
  • Recursion order is forced — pre-order: left then right; post-order consumed from the end: right then left. Getting it wrong mirrors the tree silently.
  • CostO(n)O(n) with the map, O(n2)O(n^2) with index() per node, O(n2)O(n^2) time and space with slicing.
  • Assumes distinct values — with duplicates the reconstruction is not unique, so the problem is ill-posed rather than the code being wrong.
  • Pre-order + post-order alone is not enough (unless the tree is full) — there is no way to tell a single left child from a single right one.
  • One traversal gives roots; another gives the split. Preorder or postorder supplies the root, inorder supplies the boundary. Neither alone is enough.
  • pre+in and post+in always work. pre+post works only for full trees. A BST’s preorder alone works.
  • Precompute a value-to-inorder-index mapO(1)O(1) lookups instead of O(n)O(n) scans, turning O(n2)O(n^2) into O(n)O(n). Requires distinct values.
  • Use one advancing pointer, never array slices — slicing costs O(n2)O(n^2) in both time and space.
  • Postorder builds right before left, because reversed postorder is root, right, left. Getting it wrong yields a plausible mirrored tree.
  • Rule-based construction (LC 654) often has a monotonic stack solution that beats the obvious O(n2)O(n^2) recursion.

Next: Serialize, Compare and Subtree — turning trees into strings, and the problems that become easy once you can.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading