Problem overview
Two integer arrays describe the same binary tree, and all values are different. preorder lists each node before its left subtree and its right subtree. inorder lists the left subtree, then the node, then the right subtree. The function buildTree(preorder, inorder) must rebuild the tree and return its root as a TreeNode?.
The lesson provides TreeNode: a class with an Int property value and two nullable properties, left and right, both null at first.
Example: preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7] give the tree [3,9,20,null,null,15,7]. The tests print a tree level by level, with null for missing children and the trailing nulls left out. Here 3 is the root, 9 is its left child, and 20 is its right child with the children 15 and 7.
Approach
Two facts do all the work:
- The first value of
preorderis the root of the whole tree. - In
inorder, the root sits between its left subtree and its right subtree. Everything to its left belongs to the left subtree, everything to its right to the right subtree.
In the example, 3 is first in preorder. In inorder, 9 is left of 3 and 15, 20, 7 are right of it. So the left subtree holds one value, and the right subtree holds three. The same idea applies inside each part: the next unused value in preorder is the root of the next subtree, and its position in inorder splits that subtree's range in two.
A direct version searches inorder for each root with a loop, which costs O(n) per node and O(n squared) overall in the worst case, such as a tree shaped like a line. Because all values are different, each value has exactly one position in inorder, so we can record every position once in a HashMap and answer each lookup in O(1). That brings the whole job to O(n).
One more detail makes the recursion simple. Preorder visits a subtree's root first, then all of its left subtree, then all of its right subtree. If the recursion builds the left subtree before the right one, it uses the values of preorder strictly from left to right. A single counter that only moves forward is enough, and no ranges of preorder have to be passed around.
Step by step
- Keep the positions. Make a
HashMap<Int, Int>namedinorderIndexas a property of the class, so the helper can read it as well. - Keep the counter. Add
var preIndex = 0, the position inpreorderof the next root to use. - Fill the map. In
buildTree, loop overinorder.indicesand storeinorderIndex[inorder[i]] = i. - Start the recursion. Return
build(preorder, 0, inorder.size - 1). The pair0andinorder.size - 1are the first and last positions ofinorder: the whole tree. - The helper
build. It builds the subtree whose values fillinorderfromlotohi. Iflo > hi, the range is empty, so returnnull: that is how the recursion ends below a leaf. - Take the root. Read
preorder[preIndex]intovalue, then movepreIndexforward by one, and createTreeNode(value). - Split the range. Look up
mid = inorderIndex[value]!!. Positionslotomid - 1are the left subtree andmid + 1tohithe right subtree. - Build the left subtree first, then the right one, and attach them as
node.leftandnode.right. The order is not a style choice: when the left call returns, it has used exactly the left subtree's roots, sopreIndexpoints at the right subtree's root. - Return
node.
The solution in Kotlin
class Solution {
val inorderIndex = HashMap<Int, Int>()
var preIndex = 0
fun build(preorder: IntArray, lo: Int, hi: Int): TreeNode? {
if (lo > hi) {
return null
}
val value = preorder[preIndex]
preIndex++
val node = TreeNode(value)
val mid = inorderIndex[value]!!
node.left = build(preorder, lo, mid - 1)
node.right = build(preorder, mid + 1, hi)
return node
}
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
for (i in inorder.indices) {
inorderIndex[inorder[i]] = i
}
return build(preorder, 0, inorder.size - 1)
}
}
Kotlin details worth knowing
- Properties are shared state.
inorderIndexandpreIndexbelong to theSolutionobject, so every call ofbuildsees the same map and the same counter. That is what lets the counter advance across the recursive calls.valmeans the property always holds the same map (which can still grow), whilevarletspreIndexchange. - A fresh object per tree. Because
preIndexis never reset, oneSolutionobject is meant to build one tree. CallingbuildTreea second time on the same object starts with the counter where the first call left it, so the second call reads the wrong values or runs off the end ofpreorder: building[1,2]/[2,1]twice with one object throws anArrayIndexOutOfBoundsExceptionthe second time. Create a newSolution()for each tree, or setpreIndex = 0at the start ofbuildTreein code of your own. !!on a map lookup.inorderIndex[value]has the typeInt?, because a map returnsnullfor a missing key. The compiler rejectsval mid: Int = inorderIndex[value]withinitializer type mismatch: expected 'Int', actual 'Int?'.The!!says the key is there, which is true for valid input. It throws aNullPointerExceptionif it isn't, for example for apreorderthat holds a value missing frominorder.IntArrayandindices.inorder.indicesis the range0..inorder.size - 1, so the loop needs no manual bounds. The array parameters are used as they come: nothing is copied or sliced.- Return type
TreeNode?.buildmay returnnullfor an empty range, so its type must be nullable, and so mustnode.leftandnode.right, which receive its result.
Complexity
- Time: O(n). Filling the map visits each position of
inorderonce. Thenbuildcreates exactly one node per value, and each call does O(1) work: one array read, one map lookup and two recursive calls. A call with an empty range does nothing but return, and there are n + 1 of those. - Space: O(n). The map stores n entries and the tree itself has n nodes. The recursion goes as deep as the tree is tall, up to n levels for a line, and the constraints keep it at 400.
Edge cases and common mistakes
- Building the right subtree first. With the counter approach, the order of the two recursive calls is fixed by preorder. Swap them and the values land in the wrong subtrees.
- Off-by-one in the ranges. The left part is
lotomid - 1and the right part ismid + 1tohi: the root atmidbelongs to neither. - One node. With
[-1]and[-1], the root's range splits into two empty ranges and the tree is just[-1]. Negative values are fine because they are only stored and looked up. - A tree that is a line. The same
preorder = [1,2,3]gives different trees for differentinorderarrays:[3,2,1]gives[1,2,null,3](only left children), and[1,2,3]gives[1,null,2,null,3](only right children). - Repeated values. The method relies on all values being different. With duplicates, a value would have several positions and the map would keep only the last one.
Testing
These results come from running the solution with a new Solution object for each pair:
preorder | inorder | Tree |
|---|---|---|
[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,4,5,3] | [4,2,5,1,3] | [1,2,3,4,5] |
