Problem overview
buildTree receives two arrays, preorder and inorder, which list the values of the same binary tree in two different orders, and returns the tree's root as a TreeNode. The lesson provides that class: new TreeNode(value) creates a node, and its left and right fields start as null and can be assigned. All values are different.
- Preorder lists a node first, then its whole left subtree, then its whole right subtree.
- Inorder lists a node's whole left subtree, then the node, then its whole right subtree.
For preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7] the answer is the tree with root 3, left child 9, and right child 20 whose children are 15 and 7. Printed level by level, as the tests do, that is [3,9,20,null,null,15,7].
Approach: preorder picks the root, inorder splits the rest
The first value of preorder is always the root. Find that value in inorder: everything to its left belongs to the left subtree, and everything to its right belongs to the right subtree. Each of those two parts is a smaller instance of the same problem, so recursion fits.
The roots of the smaller trees come from preorder too. After the root, preorder lists the whole left subtree and then the whole right subtree. So if the code builds the left subtree first and always takes the next unused value of preorder as the next root, each subtree finds its root at the right moment. That needs only a counter that moves forward through preorder.
The slow part would be finding the root in inorder again for every node, which is a linear search each time. Because all values are different, each value has one position, so a map from value to position, built once, answers in constant time. That is what brings the total to O(n).
The recursion works on ranges of inorder rather than copies of the arrays: lo and hi are the first and last index of the part that holds the current subtree's values. A range with lo > hi is empty and stands for no tree.
Step by step
- Keep the positions. Declare a field
inorderIndex, an emptyscala.collection.mutable.HashMap[Int, Int](). As a field, the helper can read it as well. - Keep the counter. Declare a
var preIndex = 0: the position inpreorderof the next root to use. - Fill the map. In
buildTree, loop overinorder.indices, and for each indexistoreinorderIndex(inorder(i)) = i. This is one pass,O(n). - Start the recursion. Return
build(preorder, 0, inorder.length - 1): the whole ofinorderis the whole tree. - Write the helper's header.
buildtakes thepreorderarray and twoInts,loandhi, and returns aTreeNode. - Stop on an empty range. If
lo > hi, returnnull. Below every leaf, both ranges are empty, so this is where the recursion ends. - Take the next root. Read
preorder(preIndex)intovalueand movepreIndexforward by one. - Create the node with
new TreeNode(value). - Find the root in
inorder.mid = inorderIndex(value). Positionslotomid - 1are the left subtree andmid + 1tohithe right one. - Build the left subtree first and assign it to
node.left. Its roots come right after this node in preorder, so the counter must serve it before the right subtree. - Build the right subtree and assign it to
node.right. By now the left subtree has used all of its roots, so the counter points at the right subtree's root. - Return the node.
The solution in Scala
class Solution {
val inorderIndex = scala.collection.mutable.HashMap[Int, Int]()
var preIndex = 0
def build(preorder: Array[Int], lo: Int, hi: Int): TreeNode = {
if (lo > hi) {
return null
}
val value = preorder(preIndex)
preIndex += 1
val node = new TreeNode(value)
val mid = inorderIndex(value)
node.left = build(preorder, lo, mid - 1)
node.right = build(preorder, mid + 1, hi)
return node
}
def buildTree(preorder: Array[Int], inorder: Array[Int]): TreeNode = {
for (i <- inorder.indices) {
inorderIndex(inorder(i)) = i
}
return build(preorder, 0, inorder.length - 1)
}
}
Following the example, here is each call of build as it runs on preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7], with the range it receives and, for a real node, the value it takes and that value's index in inorder:
lo=0 hi=4 value=3 mid=1
lo=0 hi=0 value=9 mid=0
lo=0 hi=-1 -> empty
lo=1 hi=0 -> empty
lo=2 hi=4 value=20 mid=3
lo=2 hi=2 value=15 mid=2
lo=2 hi=1 -> empty
lo=3 hi=2 -> empty
lo=4 hi=4 value=7 mid=4
lo=4 hi=3 -> empty
lo=5 hi=4 -> empty
The values 3, 9, 20, 15 and 7 are taken in exactly the order of preorder, which is why a single counter is enough.
Scala details worth knowing:
- Updating a map.
inorderIndex(inorder(i)) = iis shorthand for callingupdateon the map: it stores the value under that key. ReadinginorderIndex(value)throws aNoSuchElementExceptionfor a missing key, which can't happen here because every value ofpreorderis also ininorder. For a key that might be missing,getreturns anOption. valandvaron fields.inorderIndexis aval: the field always refers to the same map, but the map itself is mutable and grows.preIndexis avarbecause the number changes.- Arrays are indexed with parentheses:
preorder(preIndex), not square brackets.inorder.indicesis the range0 until inorder.length. - Fields keep their state.
preIndexandinorderIndexlive as long as theSolutionobject. A fresh object per tree works as shown above. CallingbuildTreea second time on the same object continues from the oldpreIndexand runs past the end ofpreorder, which throws anArrayIndexOutOfBoundsException.
Complexity
Time: O(n). Filling the map visits each index once. build is called once for each of the n nodes plus once for each empty child, which is at most n + 1 calls, and each call does constant work because the map lookup is constant time. Searching inorder from scratch at every node would cost O(n) per node and O(n²) in total for a tall tree.
Space: O(n). The map holds one entry per value, and the recursion goes as deep as the tree, at most n levels (the lesson limits it to 400).
Edge cases and common mistakes
- One value.
preorder = [-1],inorder = [-1]gives[-1]: both ranges below the root are empty. - Building the right subtree first. The counter would hand the left subtree's roots to the right side. The left call must come first.
- Same preorder, different inorder. With
preorder = [1,2,3],inorder = [3,2,1]gives[1,2,null,3], a line going left.inorder = [1,2,3]gives[1,null,2,null,3], a line going right.inorder = [2,1,3]gives a full tree,[1,2,3]. Inorder is what decides the shape. - Wrong range bounds. The root itself is at
mid, so the left range ends atmid - 1and the right range starts atmid + 1. Usingmidin either one puts the root into its own subtree and the recursion never ends. - Passing the preorder range down. Only
inorderneeds a range. Preorder is read through the shared counter, so there's no need to compute where its parts start and end.
Testing
Results from running the solution, printed level by level:
| 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] |
[1,2,3,4] | [3,2,4,1] | [1,2,null,3,4] |
