Problem overview
build_tree receives two vectors, preorder and inorder, which are the preorder and inorder traversals of the same binary tree. All values are different. The function returns the root of that tree as an Option<Box<TreeNode>>. The lesson provides TreeNode, with the fields val, left and right, and TreeNode::new(val), which makes a node with no children.
Preorder visits a node, then its whole left subtree, then its whole right subtree. Inorder visits the left subtree, then the node, then the right subtree.
For preorder = [3,9,20,15,7] and inorder = [9,3,15,20,7] the answer is the tree [3,9,20,null,null,15,7]: 3 is the root, 9 its left child, and 20 its right child with the children 15 and 7.
Approach: the first preorder value is always a root
Preorder tells you which value is the root of any subtree: it comes first. Inorder tells you what is on each side of it: once you find the root in inorder, everything to its left belongs to the left subtree and everything to its right belongs to the right subtree.
In the example, the first preorder value is 3. In inorder it sits at index 1, so [9] is the left subtree and [15,20,7] the right one. The next preorder value, 9, is the root of the left subtree. The left subtree has no more values, so the next value, 20, is the root of the right subtree, and the same reasoning applies again.
That gives a recursion over ranges of inorder. Every subtree fills one unbroken stretch of it, so two indexes, lo and hi, describe the subtree. A global counter into preorder hands out the roots in the order the recursion needs them: a node's root first, then every root of its left subtree, then every root of its right subtree. That is exactly preorder, so the counter only moves forward.
The note asks for O(n) time. Scanning inorder to find each root would cost O(n) per node, O(n²) in total for a skewed tree. A HashMap from each value to its inorder index makes each lookup constant. It works because the values are all different.
Step by step
- Create
inorder_indexas an emptyHashMap. Rust infers the key and value types from the first insert. - Loop over
inorder.iter().enumerate(), which yields each indexiwith a reference to its value. - Insert
*valuewithi as i32. The*reads the number behind the reference. The index is ani32because the helper's ranges can end at -1 when they are empty. - Create
pre_indexaslet mut, starting at 0: the position inpreorderof the next root. - End
build_treewith the helper's result. It gets the preorder slice, the map, a mutable borrow of the counter, and the whole inorder range, 0 toinorder.len() as i32 - 1. - Declare
buildwith the slice, the map, the counter as&mut usize, andloandhi. Lending the counter mutably lets every call advance the same one. - Return
Nonewhenlo > hi. An empty range holds no values, so there is no tree. This stops the recursion below every leaf. - Take the next root as
preorder[*pre_index], then add 1 to*pre_index. - Create the node with
Box::new(TreeNode::new(value)), declaredmutbecause its children are assigned next. - Find the root in inorder with
inorder_index[&value], inmid. - Build the left subtree from the range
lotomid - 1and store it innode.left. The left call must come first, because its roots come right after this node in preorder. - Build the right subtree from
mid + 1tohi. The left call has used all of its roots, so the counter now points at the right subtree's root. - Return
Some(node)without a semicolon.
The solution in Rust
use std::collections::HashMap;
impl Solution {
fn build(preorder: &[i32], inorder_index: &HashMap<i32, i32>, pre_index: &mut usize, lo: i32, hi: i32) -> Option<Box<TreeNode>> {
if lo > hi {
return None;
}
let value = preorder[*pre_index];
*pre_index += 1;
let mut node = Box::new(TreeNode::new(value));
let mid = inorder_index[&value];
node.left = Self::build(preorder, inorder_index, pre_index, lo, mid - 1);
node.right = Self::build(preorder, inorder_index, pre_index, mid + 1, hi);
Some(node)
}
fn build_tree(preorder: Vec<i32>, inorder: Vec<i32>) -> Option<Box<TreeNode>> {
let mut inorder_index = HashMap::new();
for (i, value) in inorder.iter().enumerate() {
inorder_index.insert(*value, i as i32);
}
let mut pre_index = 0;
Self::build(&preorder, &inorder_index, &mut pre_index, 0, inorder.len() as i32 - 1)
}
}
Rust details worth a closer look:
- The shared counter.
pre_index: &mut usizeis a mutable borrow. Insidebuild,*pre_indexreads and writes the number it points to. Passingpre_indexinto the recursive calls hands the same borrow on; Rust reborrows it for each call, so both calls can use it one after the other. &preorderas a slice.build_treeowns aVec<i32>, and&preordercoerces to the&[i32]thatbuildasks for.- Indexing a map.
inorder_index[&value]panics if the key is missing. That can't happen when the two inputs describe the same tree. i32andusize. The map and the range bounds usei32, sincemid - 1can be -1. The counter is ausizebecause it indexespreorder. Withloandhiasusize,mid - 1would underflow whenmidis 0.- Moving the node.
Some(node)moves the box into theOption. That's why the children are set before it: after the move,nodecan no longer be used. - The
useline.HashMaplives instd::collections, so the solution begins withuse std::collections::HashMap;.
Complexity
Time: O(n). Filling the map takes one pass over inorder. After that, each call of build that isn't empty creates exactly one node and does one constant-time map lookup, and there are n such calls plus at most n + 1 empty ones.
Space: O(n). The map holds n entries, and the tree has n nodes. The recursion goes as deep as the tree, at most 400 levels in this lesson.
Edge cases and common mistakes
- One value.
[-1]and[-1]give a tree with only a root. The left range is0..=-1and the right one is1..=0, both empty. - Building the right subtree first. The counter would hand the left subtree's roots to the right side. The two calls must follow preorder's order: left, then right.
- Left-only and right-only chains.
preorder = [1,2,3]withinorder = [3,2,1]gives[1,2,null,3], because every value comes before its parent in inorder. Withinorder = [1,2,3]the result is[1,null,2,null,3]. - Searching instead of mapping. A linear search for
valueininorderworks, but costsO(n²)on a chain. - Duplicates. The map and the whole idea rest on values being different. With repeats, one value could have two positions and the split would be ambiguous.
- Forgetting
as i32. Inserting theusizeindex makes the map aHashMap<i32, usize>, and passing it tobuild, which expects&HashMap<i32, i32>, is a type error.
Testing
Printed level by level, as the lesson does, the reference solution gives:
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].preorder = [1,2,4,5,3],inorder = [4,2,5,1,3]gives[1,2,3,4,5].
