Problem overview
The method levelOrderTraversal receives the root of a binary tree and returns a List<List<int>>. Each inner list holds the values of one level of the tree, from left to right, and the inner lists go from the top level down.
In the example, the tree has 3 at the root, 9 and 20 below it, and 15 and 7 under 20. The result is [[3], [9, 20], [15, 7]]. An empty tree (a null root) gives an empty outer list.
Approach: depth-first search that remembers the level
Level order is usually taught with a queue and a breadth-first search. This lesson does the same job with plain recursion. The idea: the outer list is indexed by level, so result[level] is the list for that level. If every node knows its own depth, it can append its value to the right inner list no matter when the recursion reaches it.
Two facts make that work:
- The root is on level 0, and a child is always one level deeper than its parent, so the recursive call passes
level + 1. - If the recursion visits the left subtree before the right subtree, then within any single level the values are appended in left-to-right order. A node further left on a level is always reached before a node further right on the same level.
The only open question is when to create an inner list. The first time the recursion reaches a new depth, level equals result.Count, which means the list for that level doesn't exist yet. Because the search only ever goes one level deeper than it has been before, a single Add is enough to make it exist.
Step by step
- Create the result. An empty
List<List<int>>namedresultcollects everything. - Delegate to a helper.
Dfs(root, 0, result)fillsresultfor the whole tree, starting at level 0. - Return
result. - Declare the helper.
Dfsreturns nothing (void) and takes the node, its level and the shared result. It writes into the list the caller gave it, so there is nothing to return. - Stop at
null. An empty subtree has no values, so the method returns at once. This is also what ends the recursion below the leaves. - Add a level when needed. If
level >= result.Count, this is the first node seen on that level, so an empty inner list is added. - Record the value in
result[level]. - Recurse left with
level + 1. - Recurse right with
level + 1.
The solution in C#
class Solution {
public void Dfs(TreeNode root, int level, List<List<int>> result) {
if (root == null) {
return;
}
if (level >= result.Count) {
result.Add(new());
}
result[level].Add(root.val);
Dfs(root.left, level + 1, result);
Dfs(root.right, level + 1, result);
}
public List<List<int>> levelOrderTraversal(TreeNode root) {
List<List<int>> result = new List<List<int>>();
Dfs(root, 0, result);
return result;
}
}
C# details worth knowing:
- Target-typed
new(). Inresult.Add(new())the compiler knowsAddexpects aList<int>, sonew()creates exactly that. It is shorthand fornew List<int>()and needs C# 9 or later. - Lists are reference types. The caller and
Dfshold references to the sameListobject, so everyAddinside the recursion is visible inlevelOrderTraversal. That is whyDfscan bevoidand norefis needed. - Indexing a list.
result[level]reads the inner list at that position, and.Add(root.val)appends to it. An index that is out of range throwsArgumentOutOfRangeException, which is why the level must be created first. - Names. The lesson's method is
levelOrderTraversal, starting with a lowercase letter, and the fields areval,leftandright. C# is case-sensitive, so keep them exactly as given. Many C# programs use PascalCase for method names, but a call toLevelOrderTraversalwouldn't find this method.
Complexity
Time: O(n). Every node is visited exactly once, and each visit does constant work: one comparison, at most one Add of an empty list, and one Add of a value.
Space: O(n). The result holds all n values. On top of that, the recursion uses stack space proportional to the height of the tree: O(log n) for a balanced tree and O(n) for one that leans entirely to one side. A queue-based breadth-first search has the same time and also uses O(n) space.
Edge cases and common mistakes
- Empty tree. With a
nullroot,Dfsreturns immediately and the method returns an empty list, notnull. - Single node. The result is
[[5]]for a tree holding just 5. - Skewed tree. A tree where each node has only a right child gives one value per level, such as
[[1], [2], [3]]. - Visiting right before left. The levels would come out right to left. Keep the left call first.
- Using
level > result.Count. That never creates the first list, because on level 0 with an empty result the condition is false, andresult[0]then throws. - Creating a list on every visit. That would give each node its own list and lose the grouping. The new list is added only when the level is new.
- Forgetting the null check. Reading
root.valon anullnode throwsNullReferenceException.
Testing
- The tree
[3, 9, 20, null, null, 15, 7]gives[[3], [9, 20], [15, 7]]. - A tree with only the root 1 gives
[[1]]. - A
nullroot gives[].