Problem overview
buildTree receives two arrays that describe the same binary tree, preorder and inorder, and must return the root of that tree. All values are different. Preorder lists a node first, then its whole left subtree, then its whole right subtree. Inorder lists the left subtree, then the node, then the right subtree.
The lesson provides the TreeNode class: it has val, left and right fields, and its constructor takes just the value and starts both children as null. The tests print the returned tree level by level, with null for missing children and the trailing nulls dropped.
For preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7], the solution returns the tree printed as [3,9,20,null,null,15,7]: root 3, left child 9, right child 20, and 20 has children 15 and 7.
Approach: preorder names the roots, inorder splits the sides
Two facts do all the work.
- The first value of
preorderis the root. After it come the left subtree's roots, and only then the right subtree's. - In
inorder, the root sits between its two subtrees. Everything to its left is the left subtree and everything to its right is the right subtree.
So the first preorder value gives the root, and its position in inorder tells us which values go left and which go right. Each subtree is the same problem on a smaller stretch of inorder, and its root is the next unused value in preorder.
The slow way is to search for the root inside the current stretch of inorder each time, which costs O(n) per node and O(n²) in total for a long, thin tree. Because the values are all different, we can instead record every value's position in inorder once, in a Map, and look it up in constant time.
Step by step
- The map. A property
inorderIndex, aMap<number, number>, will hold each value's index ininorder. - The preorder counter. A property
preIndex, starting at 0, is the position inpreorderof the next root to use. Preorder lists roots in exactly the order we build them, so one counter that only moves forward is enough. - Fill the map. In
buildTree, loop overinorderand callthis.inorderIndex.set(inorder[i], i)for each index. This costsO(n). - Start the recursion. Return
this.build(preorder, 0, inorder.length - 1). The pairloandhinames the stretch ofinorderthat holds the current subtree's values, and the whole array is the whole tree. - Empty range. In
build,lo > himeans no values, so returnnull. This is where the recursion stops below every leaf. - Take the root. Read
preorder[this.preIndex]intovalue, then increasethis.preIndex. - Make the node with
new TreeNode(value). - Find the split.
this.inorderIndex.get(value)givesmid, the root's position ininorder. Indiceslotomid - 1are the left subtree andmid + 1tohithe right one. - Build the left subtree first, from
lotomid - 1. The order matters: inpreorder, the left subtree's roots follow this node directly. - Then the right subtree, from
mid + 1tohi. By then the left recursion has used all its roots, sopreIndexpoints at the right subtree's root. - Return the node.
The solution in TypeScript
class Solution {
inorderIndex: Map<number, number> = new Map();
preIndex: number = 0;
build(preorder: number[], lo: number, hi: number): TreeNode | null {
if (lo > hi) {
return null;
}
let value = preorder[this.preIndex];
this.preIndex++;
let node = new TreeNode(value);
let mid = this.inorderIndex.get(value);
node.left = this.build(preorder, lo, mid - 1);
node.right = this.build(preorder, mid + 1, hi);
return node;
}
buildTree(preorder: number[], inorder: number[]): TreeNode | null {
for (let i = 0; i < inorder.length; i++) {
this.inorderIndex.set(inorder[i], i);
}
return this.build(preorder, 0, inorder.length - 1);
}
}
TypeScript details
inorderIndexandpreIndexare properties with initial values, so every method sees them throughthis. They live as long as theSolutionobject does.Map.prototype.getreturnsnumber | undefined. The lesson compiles with--strict false, somid - 1is accepted. Under strict mode the compiler would complain thatmidmay beundefined. Here the lookup never fails, because every value inpreorderalso appears ininorder.- The types are erased when the code compiles to JavaScript. Nothing checks at run time that the two arrays really come from the same tree.
- Arrays printed in the tests are plain numbers, so
-1and other negatives need no special treatment.
Tracing the example
Calling build on preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7] and printing each call that creates a node gives:
lo | hi | preIndex on entry | root taken |
|---|---|---|---|
| 0 | 4 | 0 | 3 |
| 0 | 0 | 1 | 9 |
| 2 | 4 | 2 | 20 |
| 2 | 2 | 3 | 15 |
| 4 | 4 | 4 | 7 |
The calls between them, such as lo = 0, hi = -1, have an empty range and return null without moving preIndex. Notice that the roots are taken in the order 3, 9, 20, 15, 7, which is exactly preorder.
Complexity
Time: O(n). Filling the map takes one pass. build creates each node once and does constant work per call besides the two recursive calls, and there are about 2n + 1 calls, counting the ones that return null.
Space: O(n) for the map, plus the recursion stack, which is as deep as the tree (at most 400 levels here) and the nodes that are returned.
Edge cases and common mistakes
- Building the right side first. The counter moves through
preorderin order, so the left subtree has to be built before the right one. Swapping the two lines attaches wrong values. - Passing the wrong ranges. The root at
midis excluded from both sides:mid - 1andmid + 1, notmid. - Searching for the root every time. It works, but costs
O(n²)on a long chain. The map avoids that. - Reusing a
Solutionobject. The map and the counter are stored in the object. A second call tobuildTreeon the same object starts withpreIndexleft where the first call ended. With[1,2]and[2,1], the first call returns the tree[1,2]and the second one fails with "Maximum call stack size exceeded". Create a newSolutionfor each tree. - One-node trees.
[-1]and[-1]give a node with twonullchildren, printed as[-1].
Testing
Each pair below was run on a fresh Solution and the tree printed in level order:
preorder | inorder | Result |
|---|---|---|
[3,9,20,15,7] | [9,3,15,20,7] | [3,9,20,null,null,15,7] |
[-1] | [-1] | [-1] |
[1,2,3] | [3,2,1] | [1,2,null,3] |
[1,2,3] | [1,2,3] | [1,null,2,null,3] |
[1,2,3] | [2,1,3] | [1,2,3] |
