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.
- 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
inorder = [9,3,15,20,7], postorder = [9,15,7,20,3][3,9,20,null,null,15,7]inorder = [2,1], postorder = [2,1][1,2]inorder = [1,2], postorder = [2,1][1,null,2]inorder = [1], postorder = [1][1]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.
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.
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.)
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)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.
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.idx now points at 7.idx points at 15.idx points at 9.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.
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.
Inorder + Postorder
Recursive Slicing Logic