Problem overview
buildTree(preorder, inorder) receives two arrays that list the same tree's values in two orders and returns the tree's root. Preorder lists a node, then its whole left subtree, then its whole right subtree. Inorder lists the left subtree, then the node, then the right subtree. All values are different. The lesson provides TreeNode, which is built as new TreeNode(val) and has val, left and right fields, the last two starting as null.
For preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7], the tests print the tree level by level as [3,9,20,null,null,15,7]: the root is 3, with 9 on the left and 20 on the right, and 20 has the children 15 and 7.
Approach: preorder names the roots, inorder splits the sides
Preorder always starts with the root. Inorder then shows where that root sits: everything to its left belongs to the left subtree and everything to its right belongs to the right subtree. In the example the root 3 is at index 1 of inorder, so [9] is the left subtree and [15,20,7] is the right one.
The same idea repeats on each side. The next value in preorder after the root is the root of the left subtree, and once the left subtree is used up, the value after it is the root of the right subtree. So one counter walking through preorder gives the roots in exactly the order the recursion needs them, and a pair of indexes lo and hi says which stretch of inorder the current subtree occupies.
The only repeated question is "where is this value in inorder?". Searching the array each time costs O(n) per node, which is O(n²) in total for a long chain. Because the values are all different, a Map from value to index, filled once, answers in O(1).
Step by step
- Create the map as
this.inorderIndex = new Map(). A property onthisis visible to every method, so the helper can read it. - Loop over
inorderwithifrom 0 whilei < inorder.length. - Store each pair with
this.inorderIndex.set(inorder[i], i). - Start the preorder counter with
this.preIndex = 0: the position of the next root to use. - Return the helper's result for the whole of
inorder:this.build(preorder, 0, inorder.length - 1). - Declare the helper
build(preorder, lo, hi).loandhiare the first and last index of the part ofinorderholding this subtree. - Stop on an empty range: if
lo > hi, returnnull. Below a leaf, both ranges are empty. - Take the next root: read
preorder[this.preIndex]intovalue, then move the counter forward. - Create the node with
new TreeNode(value). - Find the split:
midisthis.inorderIndex.get(value). - Build the left subtree first from
lotomid - 1. Its roots come right after this node in preorder. - Then the right subtree from
mid + 1tohi. The left call has used all its roots, so the counter points at the right subtree's root. - Return the node.
The solution in JavaScript
class Solution {
build(preorder, lo, hi) {
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, inorder) {
this.inorderIndex = new Map();
for (let i = 0; i < inorder.length; i++) {
this.inorderIndex.set(inorder[i], i);
}
this.preIndex = 0;
return this.build(preorder, 0, inorder.length - 1);
}
}
Details worth knowing:
- Order matters. The left subtree must be built before the right one, because the counter only works if it is consumed in preorder. Swapping the two
buildlines pairs the roots with the wrong ranges. - State on
this.preIndexandinorderIndexare properties of the object, set at the start ofbuildTree. Each call tobuildTreeresets them, so oneSolutionobject can be used for several inputs. Mapinstead of an object. AMapworks with number keys directly, andgetreturnsundefinedfor a missing key. Here every value is present, because both arrays hold the same values.- The counter is shared.
this.preIndex++changes the property in place, and the next call, even one made in the recursion, sees the new value.
Complexity
Time: O(n). Filling the map takes n steps. Each call of build that doesn't return early creates exactly one node, so there are n of those and at most n + 1 calls that return null. Each call does a constant amount of work, because the map lookup is O(1).
Space: O(n). The map holds n entries, and the recursion goes as deep as the tree, which is at most n levels (400 in this lesson).
Edge cases and common mistakes
- A single value.
preorder = [-1]withinorder = [-1]gives[-1]: the root's two ranges are empty, so both children arenull. - All left children.
preorder = [1,2,3]withinorder = [3,2,1]gives[1,2,null,3], a chain going left. - All right children. The same preorder with
inorder = [1,2,3]gives[1,null,2,null,3]. - Building the right subtree first. This reads the wrong values from
preorder. - Using
midinstead ofmid - 1andmid + 1. The root's own position would be included in a subtree, so its value would be placed a second time. - Passing the counter by value. If
preIndexwere a local variable passed as an argument, the right call would not see how far the left call had read. Keeping it onthisavoids that.
Testing
preorder = [3,9,20,15,7],inorder = [9,3,15,20,7]gives[3,9,20,null,null,15,7].preorder = [-1],inorder = [-1]gives[-1].preorder = [1,2,3],inorder = [3,2,1]gives[1,2,null,3].preorder = [1,2,3],inorder = [1,2,3]gives[1,null,2,null,3].preorder = [1,2,3],inorder = [2,1,3]gives[1,2,3].
