Algorithm

Construct Tree from Inorder and Postorder

Trees Pattern

Construct Tree from Inorder and Postorder

You are given inorder, the values of a binary tree in the order an in-order traversal produces them, and postorder, the values in post-order. Rebuild the tree and return its root. All values are distinct, and the two arrays are guaranteed to come from the same tree, which they determine uniquely.

CONSTRAINTS
  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • All values in the tree are unique
  • inorder and postorder are guaranteed to be valid traversals of the same tree
EXAMPLE 1
Input: inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
Output: [3,9,20,null,null,15,7]
Post-order's last value is the root, 3. In-order then shows one value before it, so the left subtree holds only 9 and the other three belong on the right.
EXAMPLE 2
Input: inorder = [2,1], postorder = [2,1]
Output: [1,2]
The root is 1 (last in post-order), and 2 precedes it in-order, so 2 is the left child.
EXAMPLE 3
Input: inorder = [1,2], postorder = [2,1]
Output: [1,null,2]
Post-order is identical to the previous case, yet the tree differs — the in-order array alone decides which side 2 lands on.
EXAMPLE 4
Input: inorder = [1], postorder = [1]
Output: [1]
A single node is both the root and the whole tree, and the recursion's empty-range base case handles its two absent children.
How does this differ from the pre-order version?
Only in where the root sits: last in post-order instead of first in pre-order. That one change flips the order in which the two subtrees must be built.
Could I just reverse the post-order array and reuse the previous solution?
Nearly. Reversed post-order reads root, right, left — a mirrored pre-order — so the same code works if you also swap the roles of left and right. Many people find it cleaner to consume post-order from the back directly.
Are duplicate values allowed?
No, and the method depends on that: the root must be locatable in the in-order array by value, which requires exactly one occurrence.
Does the in-order array still carry the shape information?
Yes, exactly as before. Post-order names the roots; in-order says where each one splits its range.

This is the same reconstruction as Construct Tree from Preorder and Inorder, with one component swapped. The division of labour is unchanged: in-order tells you where a known root splits its range, and the other array tells you which value is the root. Everything interesting here is in what changes when that other array is post-order.

The root moved to the other end

Post-order visits left subtree, right subtree, node — so the node comes last. For the whole tree, the root is therefore the final element of the array, and for any contiguous segment of post-order that corresponds to some subtree, that subtree's root is the last element of the segment.

So take the last value, find it in the in-order range, and the split works exactly as before: values before it form the left subtree, values after it the right.

Why the right subtree must be built first

Here is the one genuinely new wrinkle, and it is easy to get wrong.

The tidy way to consume post-order is to keep a single index walking backwards from the end, taking one root per call. But look at what lies immediately before the root inside its segment: the segment reads [left subtree] [right subtree] [root], so stepping back from the root lands you in the right subtree, not the left. If the code recurses left first, that call will consume values belonging to the right subtree and hang them on the wrong side.

So the calls must be ordered right, then left. Nothing about the tree changed — this is purely about matching the order in which the array hands values over. (In the pre-order version the analogous constraint pointed the other way, because pre-order reads [root] [left] [right] forwards.)

python
pos = {v: i for i, v in enumerate(inorder)}   # value -> index in inorder
idx = len(postorder) - 1                       # walks backwards, one root per call

def build(lo, hi):                             # inclusive in-order range
    nonlocal idx
    if lo > hi:
        return None                            # empty range: no subtree here
    root = Node(postorder[idx])
    idx -= 1                                   # consume this root
    mid = pos[root.val]                        # where it splits the in-order range
    root.right = build(mid + 1, hi)            # RIGHT first — see the note
    root.left  = build(lo, mid - 1)
    return root

return build(0, len(inorder) - 1)
Crucial Notewith a single shared idx, the sequence of calls is what keeps everything aligned — there are no explicit post-order boundaries to check against, so a mistake produces a plausible-looking tree rather than an error. Swap the two recursive lines and the right subtree's values silently become the left subtree. If you would rather not rely on call order, pass explicit ranges for the post-order array as well, computed from left_size = mid - lo exactly as in the pre-order version; that is more code but it fails loudly when wrong.

The lookup map plays the same role and earns its keep for the same reason: scanning the in-order range for each root would cost O(N) per node and O(N²) overall, while a value-to-index map built once makes each split a constant-time question. It works only because the values are unique.

Worked example:inorder = [9, 3, 15, 20, 7], postorder = [9, 15, 7, 20, 3]
- Range [0..4]. idx points at 3, the last value — the root. It sits at in-order index 1, so the left range is [0..0] (just 9) and the right range is [2..4] ([15, 20, 7]). Consume 3; idx now points at 20.
- Build right first, range [2..4]. Root is 20, at in-order index 3, so its left range is [2..2] (15) and its right range is [4..4] (7). Consume 20; idx now points at 7.
- Right of 20, range [4..4]. Root is 7. Both its sub-ranges are empty, so it is a leaf. Consume 7; idx points at 15.
- Left of 20, range [2..2]. Root is 15, a leaf. Consume 15; idx points at 9.
- Now back at the top, build the left, range [0..0]. Root is 9, a leaf.
- Result: 3 with children 9 and 20; 20 with children 15 and 7.

Trace idx across those steps — 3, 20, 7, 15, 9 — and you have read the post-order array backwards, which is root, right, left. That reversed reading is exactly the order the recursion demanded.

Cost, and the family

One node created per call, constant work each with the map: O(N) time, O(N) space for the map plus O(H) for the recursion. Without the map, O(N²).

Two reconstructions, one skeleton: find the root, split the range, recurse on the pieces. What varies is only where the root hides in the auxiliary array and, consequently, which subtree must be consumed first. Worth knowing the boundary of this family too — pre-order paired with post-order does not work, because when a node has one child neither array reveals which side it is on, so the shape stays ambiguous exactly as it did with a single traversal. In-order is doing the load-bearing work in both problems here; the next problem, Serialize and Deserialize, shows the alternative escape route — record the nulls explicitly and a single traversal becomes enough on its own.

Interactive Strategy Visualization

Inorder + Postorder

Recursive Slicing Logic

Postorder Pick
9
15
7
20
3
Inorder Slicing
9
3
15
20
7
LEFT SUBTREE
ROOT
RIGHT SUBTREE
Subtree State
Starting construction. Look at last element of postorder.
CODE ANCHOR
rootNode = new Node(postorder[idx--]);
O(N²) Scan In-order For Each Root
O(N) Time · O(N) Space Hash Map With Backward Index