Problem overview
The function inorder_traversal receives the root of a binary tree, an Option<Box<TreeNode>>, and returns the values of all nodes as a Vec<i32> in inorder: the whole left subtree first, then the node itself, then the whole right subtree.
In the example, node 1 has no left child and has node 2 as its right child, and node 2 has node 3 as its left child. The traversal visits 1 first, because its left subtree is empty. Then it enters the right subtree rooted at 2, where the left child 3 comes before 2 itself. The result is [1, 3, 2].
Approach: let recursion follow the shape of the tree
A tree is a node with two smaller trees attached, so the definition of inorder is already recursive. To traverse a tree: traverse its left subtree, record the node, traverse its right subtree. An empty tree is the base case and records nothing.
In a binary search tree this order yields the values sorted ascending, which is the main reason inorder is worth knowing. The other two depth-first orders only move the "record the node" step: before both subtrees is preorder, after both is postorder.
The statement's follow-up asks for an iterative version. This lesson does the recursive one, which is short enough to write from the definition. The iterative idea is mentioned at the end.
Step by step
- Write the helper
dfs. It takes the root of a subtree, as anOption<Box<TreeNode>>, and a&mut Vec<i32>namedresult. The mutable reference lets every recursive call push into the same vector, so nothing has to be merged afterwards. The helper has noself, so it is called asSelf::dfs. - Unwrap the root with
if let Some(node). When the subtree is empty the pattern doesn't match, the block is skipped, and the function returns. That is the base case. - Recurse into the left subtree with
node.left. - Push
node.valintoresult. - Recurse into the right subtree with
node.right. The three steps in this order are what make it inorder. - In
inorder_traversal, create an emptyresultvector withlet mut, callSelf::dfs(root, &mut result)and returnresult.
The solution in Rust
impl Solution {
fn inorder_traversal(root: Option<Box<TreeNode>>) -> Vec<i32> {
let mut result = Vec::new();
Self::dfs(root, &mut result);
result
}
fn dfs(root: Option<Box<TreeNode>>, result: &mut Vec<i32>) {
if let Some(node) = root {
Self::dfs(node.left, result);
result.push(node.val);
Self::dfs(node.right, result);
}
}
}
The Rust details worth a closer look:
- Ownership moves down the tree.
dfstakesrootby value, so it owns that subtree. Passingnode.leftmoves the left child out of the box into the recursive call, andnode.rightdoes the same later.node.valis ani32, which is copied, so reading it between the two moves is fine. The tree is consumed by the traversal, and the caller can't use it afterwards. &mut Vec<i32>is passed on as it is.resultis already a mutable reference insidedfs, so the recursive calls takeresult, not&mut result. Rust reborrows it for each call, which is why the same reference can be used again afterwards.- The type of
Vec::new()is inferred. The call todfssays the vector holdsi32, and the function returns aVec<i32>, so no annotation is needed. - Order of definition doesn't matter.
dfsis written below the function that calls it. Inside animplblock, methods and associated functions can be declared in any order. - Final expression. The last line
resulthas no semicolon, so it is returned.
Complexity
Time: O(n) for n nodes. Each node is visited once, and each visit does a constant amount of work. The calls on empty subtrees add at most n + 1 more constant-time calls.
Space: O(h) for the call stack, where h is the height of the tree, plus O(n) for the result itself. A balanced tree has h near log n. A tree that is a single chain has h equal to n, and a very long chain makes the recursion very deep.
Edge cases and common mistakes
- Empty tree.
Nonereturns an empty vector. - Only left children. A chain 5, 4, 3, 2 going down the left side gives
[2, 3, 4, 5]: the deepest node is reported first. - Wrong order of the three steps. Pushing the value first gives preorder and pushing it last gives postorder. For the example tree those are
[1, 2, 3]and[3, 2, 1]. - Returning a new vector from each call. It works, but it copies values at every level. Passing one
&mut Vecavoids that. - Using
nodeafter moving its children. Afternode.leftandnode.righthave been moved, only the fields that were not moved can still be used.valis never moved, since it is copied.
The iterative idea
The recursion's call stack can be replaced by a Vec used as a stack. Walk down the left edge, pushing each node. Then pop a node, record its value, and continue with its right child. Repeat until the current subtree is empty and the stack is empty. On the same example this also returns [1, 3, 2], and it keeps the depth on the heap instead of the call stack.
Testing
- Root 1 with right child 2, whose left child is 3:
[1, 3, 2]. - Root 4 with children 2 and 6, whose children are 1, 3, 5 and 7:
[1, 2, 3, 4, 5, 6, 7]. - An empty tree:
[].
