Problem overview
The method buildTree receives two arrays, preorder and inorder, which list the values of the same binary tree in two different orders, and returns the root of that tree. All values are different. The lesson provides the TreeNode class, with val, left, right and a constructor that takes the value.
In preorder a node comes first, then its whole left subtree, then its whole right subtree. In inorder the left subtree comes first, then the node, then the right subtree.
For preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7] the tree has 3 at the root, 9 as its left child, and 20 as its right child, with 15 and 7 below 20. Level by level, the tests print it as [3,9,20,null,null,15,7].
Approach: preorder names the roots, inorder splits the sides
Each traversal alone is ambiguous. The two together are enough, because they carry different information:
- The first value of
preorderis the root of the whole tree. - In
inorder, everything left of that root belongs to its left subtree and everything right of it to its right subtree.
That splits one problem into two smaller ones of the same kind. In the example, 3 is the root. In inorder, the value 9 is left of 3, so 9 alone is the left subtree, and 15, 20, 7 are the right subtree. The right subtree's root is the next unused value of preorder, which is 20, and the same split applies again.
The root of each subtree is always the next unused value of preorder, as long as the code builds the left subtree completely before the right one. That order is exactly how preorder lists the nodes, so a single counter that moves forward through preorder is all the bookkeeping needed.
The only repeated question is "where does this value sit in inorder?". Scanning for it each time would cost O(n) per node and O(n²) overall for a tree shaped like a chain. A HashMap from value to index, filled once, answers in constant time, which gives O(n) overall. It works because the values are all different.
Step by step
- Keep a map from value to
inorderposition in a field,inorderIndex, aMap<Integer, Integer>starting as an emptyHashMap. A field is visible to the helper method too. - Keep a counter in a field,
preIndex, starting at 0: the position inpreorderof the next root to use. - Fill the map in
buildTreewith a loop over the indices ofinorder, storing each value with its index. This costsO(n)once. - Start the recursion by returning
buildover the whole ofinorder, from index 0 toinorder.length - 1. - Declare
build(preorder, lo, hi). The pairloandhiis the stretch ofinorderthat holds this subtree's values. Every subtree fills one unbroken stretch, so two numbers describe it. - Return
nullfor an empty range,lo > hi. This is where the recursion ends, below the leaves. - Take the next root: read
preorder[preIndex]intovalueand advancepreIndex. - Create the node for that value.
- Find
mid, the root's position ininorder, withinorderIndex.get(value). - Build the left subtree from the range
lotomid - 1and attach it asnode.left. This must come first, because the left subtree's roots follow this node inpreorder. - Build the right subtree from
mid + 1tohiasnode.right. By now the left side has used all of its roots, sopreIndexpoints at the right subtree's root. - Return the node.
For the example, the calls run in this order (lo and hi are positions in inorder):
lo=0 hi=4 root=3 mid=1
lo=0 hi=0 root=9 mid=0
lo=0 hi=-1 empty
lo=1 hi=0 empty
lo=2 hi=4 root=20 mid=3
lo=2 hi=2 root=15 mid=2
lo=2 hi=1 empty
lo=3 hi=2 empty
lo=4 hi=4 root=7 mid=4
lo=4 hi=3 empty
lo=5 hi=4 empty
The roots 3, 9, 20, 15, 7 come out in exactly the order of preorder.
The solution in Java
class Solution {
Map<Integer, Integer> inorderIndex = new HashMap<>();
int preIndex = 0;
TreeNode build(int[] preorder, int lo, int hi) {
if (lo > hi) {
return null;
}
int value = preorder[preIndex];
preIndex++;
TreeNode node = new TreeNode(value);
int mid = inorderIndex.get(value);
node.left = build(preorder, lo, mid - 1);
node.right = build(preorder, mid + 1, hi);
return node;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) {
inorderIndex.put(inorder[i], i);
}
return build(preorder, 0, inorder.length - 1);
}
}
Java details worth a closer look:
- Unboxing.
inorderIndex.get(value)returns anInteger, and assigning it to anintunboxes it automatically. If the key were missing,getwould returnnulland the unboxing would throw aNullPointerException. The lesson guarantees that both arrays hold the same values, so every lookup succeeds. - Autoboxing in
put.inorderIndex.put(inorder[i], i)wraps bothints intoIntegerobjects. Map keys can't be primitives. - Fields shared by the methods.
preIndexis changed insidebuildand stays changed across the recursive calls, which is what makes the single counter work. Passing it as a parameter would give each call its own copy. - Fields live as long as the object. A second call of
buildTreeon the sameSolutionobject would start withpreIndexstill at its old value and fail. Each call needs a fresh object, or the method would have to resetpreIndexand clear the map first. - Left before right. The two recursive calls are statements in a fixed order. Swapping them builds the wrong tree, because the right subtree would take the left subtree's roots from
preorder.
Complexity
Time: O(n). Filling the map takes n steps. Each value becomes exactly one node, and each node costs one map lookup and a constant amount of other work. The calls that return null number n + 1, one for each missing child.
Space: O(n). The map holds n entries and the tree itself has n nodes. The recursion adds a stack as deep as the tree, at most 400 levels in this lesson.
Edge cases and common mistakes
- A single value.
preorder = [-1]andinorder = [-1]give the tree[-1]: the root's two ranges are empty and both children arenull. - Only left children.
preorder = [1,2,3]withinorder = [3,2,1]gives[1,2,null,3]: each root sits at the right end of its range, so the right range is always empty. Withinorder = [1,2,3]the samepreordergives[1,null,2,null,3], a line going right. - Range arithmetic. The left range ends at
mid - 1and the right one starts atmid + 1. Usingmidin either one puts the root into its own subtree and the recursion never ends. - Building right first. The counter then reads the wrong roots, as described above.
- Searching instead of mapping. A loop that looks for
valueininorderreturns the right answer but makes the solutionO(n²)on a chain-shaped tree. - Using the array length for the right bound. The last valid index is
inorder.length - 1. Passinginorder.lengthmakes the first call's range too large.
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].
