Problem overview
The method inorderTraversal receives the root of a binary tree, as a TreeNode?, and returns the values of all nodes as an [Int] in inorder: for every node, first everything in its left subtree, then the node itself, then everything in its right subtree.
In the example, node 1 has no left child and a right child 2, and node 2 has a left child 3. Starting at 1, the left subtree is empty, so 1 comes first. Next comes the right subtree, rooted at 2: its left subtree holds 3, so 3 comes before 2. The result is [1, 3, 2].
Approach: let recursion follow the shape of the tree
A tree is defined in terms of smaller trees: each child is the root of a subtree. The inorder rule is defined the same way, so a recursive function fits it directly. To traverse a subtree, traverse its left subtree, record the root, then traverse its right subtree. An empty subtree, a nil child, records nothing and ends the recursion.
The statement suggests an iterative version as a follow-up. This lesson uses the recursive one, which is the shortest way to see the pattern. The other two depth-first orders only change where the "record" line sits: before the two recursive calls gives preorder, after them gives postorder.
Step by step
- Declare the helper
dfs. It takes the subtree's root and the array being filled. The array is aninoutparameter, because Swift arrays are values: withoutinoutthe function would receive its own copy, and nothing it appended would reach the caller. Both parameters use_, so calls need no argument labels. - Stop on
nil. A missing child is an empty subtree and adds nothing. This check is the base case that ends every branch of the recursion. - Recurse into the left subtree with
root!.leftand&result. The!is safe because the check above already returned whenrootwasnil, and&is required at every call that passes aninoutargument. - Append
root!.val. By now every value from the left subtree is in the array, so the current node lands right after them. - Recurse into the right subtree the same way. Left subtree, node, right subtree: that order defines inorder.
- In
inorderTraversal, declareresultwithvar. A constant can't be passed as aninoutargument. - Call
dfs(root, &result)to fill it, then returnresult.
The solution in Swift
class Solution {
func dfs(_ root: TreeNode?, _ result: inout [Int]) {
if root == nil {
return
}
dfs(root!.left, &result)
result.append(root!.val)
dfs(root!.right, &result)
}
func inorderTraversal(_ root: TreeNode?) -> [Int] {
var result = [Int]()
dfs(root, &result)
return result
}
}
The Swift details worth a closer look:
inoutand&. Aninoutparameter is copied in, changed, and copied back out when the function returns. The compiler optimises this so the array isn't actually duplicated at each call, and the recursive calls all work on the sameresult.- Optionals.
leftandrightareTreeNode?, so a missing child isnil. Comparing withroot == nilis allowed even thoughTreeNodeisn'tEquatable, and force-unwrapping with!would crash onnil, which is why the check comes first. An alternative isguard let root = root else { return }, which unwraps once and removes every!. - Classes are references.
TreeNodeis a class, so passing a node todfspasses a reference. The array, a value type, neededinout; the nodes didn't.
Complexity
Time: O(n), where n is the number of nodes. Each node is visited once, and each visit does a constant amount of work: one append and two calls. Every nil child costs one extra call that returns immediately, which at most doubles the number of calls.
Space: O(h) for the call stack, where h is the height of the tree, on top of the O(n) result. A balanced tree has h around log n. A tree that is a single chain has h = n, and a chain of millions of nodes could exhaust the stack. An iterative version with an explicit stack avoids that limit.
Edge cases and common mistakes
- Empty tree. A
nilroot returns[]. - Single node. The tree
[5]returns[5]. - Appending in the wrong place. Putting the append before the left call gives preorder,
[1, 2, 3]for the example; putting it last gives postorder,[3, 2, 1]. - Forgetting
&. The compiler rejects a call that passes an array to aninoutparameter without it. - Declaring
resultwithlet. A constant can't be passed with&, so the code doesn't compile. - Checking only the root's children. The base case belongs at the top of
dfs, so everynilis handled in one place.
Testing
- The tree
[1, null, 2, 3]gives[1, 3, 2]. - An empty tree gives
[]. - The perfect tree
[4, 2, 6, 1, 3, 5, 7], a binary search tree, gives[1, 2, 3, 4, 5, 6, 7]: the inorder traversal of a binary search tree is sorted.
