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.
What you’ll learn
Section titled “What you’ll learn”- Why a single traversal is ambiguous, and which pairs are sufficient.
- The recursive template, and the index map that removes the 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 cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”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:
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:
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.
The template — preorder + inorder
Section titled “The template — preorder + inorder”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 builds the right subtree first
Section titled “Postorder builds the right subtree first”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:
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.
Dry run
Section titled “Dry run”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.
| depth | in-order window | preorder[pre] | mid | pre after | result |
|---|---|---|---|---|---|
| 0 | [0,4] | 3 | 1 | 1 | root 3; left gets [0,0], right gets [2,4] |
| 1 | [0,0] | 9 | 0 | 2 | node 9 |
| 2 | [0,-1] | — | — | 2 | left > right → None |
| 2 | [1,0] | — | — | 2 | None — 9 is a leaf |
| 1 | [2,4] | 20 | 3 | 3 | node 20; left [2,2], right [4,4] |
| 2 | [2,2] | 15 | 2 | 4 | node 15, both children empty |
| 2 | [4,4] | 7 | 4 | 5 | node 7, both children empty |
Built tree: [3, 9, 20, null, null, 15, 7]. ✓
Four things to take from that table:
preonly ever moves forward, and it advances exactly once per node — 5 nodes,preends 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 indexespreorderby anything butpre. Mixing the two index spaces is the most common way this template goes wrong. left > rightis the base case, notleft == 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.leftmust 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.
Complexity
Section titled “Complexity”| Approach | Time | Space |
|---|---|---|
| Index map + shared pointer | for the map, recursion | |
inorder.index(root) per node | ||
| Array slicing per call | — every level copies | |
| From a rule, e.g. LC 654 max tree | naive, with a monotonic stack |
The map is the whole difference. inorder.index(v) is an scan per node, so a
degenerate tree with costs about nine million comparisons instead of three
thousand dictionary lookups. Slicing is worse still, because it also allocates: each of
the levels copies elements.
Construction from a rule
Section titled “Construction from a rule”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 — . A monotonic stack does it in :
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 maxEach node is pushed once and popped at most once, so it is . The stack bottom ends up holding the largest value — the root.
| Approach | Time | Space |
|---|---|---|
| Slicing recursion | ||
| Index map + shared pointer | ||
| Find-the-max recursion (654) | ||
| Monotonic stack (654) |
The variant map
Section titled “The variant map”| Variant | The root comes from | The split comes from | Problem |
|---|---|---|---|
| Preorder + inorder | Front of preorder | Inorder position | 105 |
| Inorder + postorder | Back of postorder | Inorder position | 106 |
| Preorder + postorder | Front of preorder | Postorder (full trees only) | 889 |
| Preorder of a BST | Front of preorder | Sort it to get inorder, or use bounds | 1008 |
| Sorted array | The middle element | Array halves | 108 |
| Maximum rule | The largest element | Monotonic stack | 654 |
| Serialised string | The token stream | Explicit null markers | 297 |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”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 . Space for the map plus 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?” ; 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 . Space .
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 .
LC 654 — Maximum Binary Tree · Medium
Section titled “LC 654 — Maximum Binary Tree · Medium”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. worst case (a sorted array), and
accepted at n <= 1000.
The 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
nmust end up inn’s left subtree, becausenis larger and appears later — so it becomes their ancestor. Popping them in order leaves the largest of them asn’s left child. - If a larger value remains on the stack,
nsits to its right and is later, sonbecomes that node’s right child (replacing whatever was there — correctly, since the replaced subtree has just been absorbed inton’s left).
Time — each node pushed once, popped at most once. Space .
[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.
LeetCode problem set
Section titled “LeetCode problem set”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.
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.
- 105Construct Binary Tree from Preorder and Inorder TraversalmediumIndex map + one advancing pointer; left before right
- 106Construct Binary Tree from Inorder and Postorder TraversalmediumConsume postorder backwards; **right before left**
- 654Maximum Binary TreemediumMonotonic stack for $O(n)$ instead of $O(n^2)$
- 889Construct Binary Tree from Preorder and Postorder TraversalmediumOnly unique for **full** trees -- no inorder means no split point
- 1008Construct Binary Search Tree from Preorder TraversalmediumFor a BST, preorder alone suffices -- recurse with `(low, high)` bounds
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why isn’t one traversal enough?” | Foundations | It records roots but not sides — [1,2] preorder fits two different trees |
| “Which pairs are sufficient?” | Precision | pre+in and post+in always; pre+post only for full trees; a BST’s preorder alone suffices |
| “Why the index map?” | Complexity | It replaces an scan per node, turning into ; it needs distinct values |
| “Why not slice the arrays?” | Hidden costs | Slicing copies, giving time and space |
| “Why right before left for postorder?” | The key detail | Reversed postorder is root, right, left — building left first mirrors the tree |
| “What if values repeat?” | Limits | The map is ambiguous and the tree is not uniquely determined |
| “Can 654 be done in ?” | Depth | Yes — a decreasing monotonic stack |
Edge-case checklist
Section titled “Edge-case checklist”- Single node — every template must handle
n == 1. - Left-only chain —
preorder [1,2,3],inorder [3,2,1]; also the 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.
Self-check
Section titled “Self-check”-
What does each of the two traversals contribute?
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.
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.
-
Why is one shared, ever-advancing `pre` pointer correct?
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.
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.
-
What does the index map `{value: inorder position}` buy, and what does it assume?
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.
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.
-
Why is the slicing version O(n²) in space as well as time?
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.
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.
-
For in-order + post-order (LC 106), why must the right child be built before the left?
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.
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.
-
Why is the base case `left > right` rather than `left == right`?
Both out-of-order windows appear in the dry run. Treating left == right as empty would silently drop every leaf.
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.
Recall card
Section titled “Recall card”- 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.
- Template —
index = {v: i for i, v in enumerate(inorder)}; onenonlocalpointer into the pre/post array;helper(left, right)over in-order indices. - Base case —
left > 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.
- Cost — with the map, with
index()per node, 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 map — lookups instead of scans, turning into . Requires distinct values.
- Use one advancing pointer, never array slices — slicing costs 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 recursion.
Next: Serialize, Compare and Subtree — turning trees into strings, and the problems that become easy once you can.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading