Problem overview
The function levelOrderTraversal receives the root of a binary tree, a TreeNode?, and returns a List<List<Int>>. Each inner list holds the values of one level of the tree, from left to right, and the outer list goes from the root's level downward.
In the example, 3 is the root, 9 and 20 are its children, and 15 and 7 are the children of 20. The answer is [[3], [9, 20], [15, 7]]. An empty tree gives an empty list.
Approach: remember each node's level
The usual way to traverse level by level is breadth-first search with a queue. This lesson uses a different idea that needs no queue: do an ordinary depth-first traversal, but pass each node's level down the recursion. The root is on level 0, its children on level 1, and in general a child is one level deeper than its parent.
The result is a list of lists where result[level] collects the values on that level. When a node on level level is reached, its value is appended to result[level]. If the traversal has never been that deep before, that inner list doesn't exist yet, so it is created first.
Order within a level comes from visiting the left child before the right child. Two nodes on the same level are reached in left-to-right order, because the traversal enters the left subtree completely before the right one.
The queue-based version visits nodes in true level order and needs extra memory for a whole level at once. The depth-first version visits nodes in a different order but still fills every level correctly, and it takes only a few lines because the recursion does the bookkeeping.
Step by step
- Create
resultas an emptyMutableList<MutableList<Int>>withmutableListOf. The outer list will gain one inner list per level. - Call
dfs(root, 0, result), assuming the helper exists. The root's level is 0. - Return
result. - Declare
dfsabovelevelOrderTraversalwith the parametersroot: TreeNode?,level: Intandresult. It returns nothing. - Return on a null root. A missing child has no values. Afterwards Kotlin smart-casts
rootto a non-nullTreeNode. - Create the level if it is new. When
level >= result.size, the traversal has just reached a depth it hasn't seen, so add an empty inner list. Because the traversal goes down one level at a time,levelcan be at mostresult.size, so one added list is always enough. - Add
root.valuetoresult[level]. - Recurse on
root.leftwithlevel + 1. - Recurse on
root.rightwithlevel + 1, after the left one so each level stays ordered left to right.
The solution in Kotlin
class Solution {
fun dfs(root: TreeNode?, level: Int, result: MutableList<MutableList<Int>>) {
if (root == null) {
return
}
if (level >= result.size) {
result.add(mutableListOf())
}
result[level].add(root.value)
dfs(root.left, level + 1, result)
dfs(root.right, level + 1, result)
}
fun levelOrderTraversal(root: TreeNode?): List<List<Int>> {
val result = mutableListOf<MutableList<Int>>()
dfs(root, 0, result)
return result
}
}
On the example, the calls run in this order: 3 creates level 0; 9 creates level 1; 20 is added to level 1; 15 creates level 2; 7 is added to level 2. Level 1 receives 9 before 20 even though 9's subtree is finished first, which shows why a depth-first walk still produces left-to-right levels.
The Kotlin details worth a closer look:
- Type inference in
result.add(mutableListOf()). The empty list has no elements to infer a type from, butresultis a list ofMutableList<Int>, so the compiler fills inmutableListOf<Int>()from the context. - Indexing with
result[level]. This is shorthand forresult.get(level). It throwsIndexOutOfBoundsExceptionwhen the level doesn't exist yet, which is why theifcomes first. - Nested mutable types. The helper needs
MutableList<MutableList<Int>>so that it can both add levels and add values to them. The function returns it asList<List<Int>>, which Kotlin accepts because its read-only list types are covariant.
Complexity
Time: O(n) for n nodes. Each node is visited once and does a constant amount of work, and each missing child costs one call that returns immediately.
Space: O(n) for the result, plus O(h) for the recursion, where h is the height of the tree. The recursion is deepest for a tree that is one long chain, where h = n.
Edge cases and common mistakes
- Empty tree.
nullreturns[], because no level is ever created. - Single node. A root of 5 returns
[[5]]. - Lopsided trees. Node 1 with left child 2 (which has left child 3) and right child 4 returns
[[1], [2, 4], [3]]. Level 2 is created while following the left side, long before the right side is visited. - Checking
level == result.sizeversus>=. Both work here, since a level can never skip ahead.>=is the safer habit. - Visiting right before left. Every level would come out reversed.
- Forgetting
level + 1. Passinglevelunchanged puts every value on level 0.
Testing
- The example tree gives
[[3], [9, 20], [15, 7]]. - A single node 5 gives
[[5]]. nullgives[].- Node 1 with left child 2 (which has left child 3) and right child 4 gives
[[1], [2, 4], [3]].