102. Binary Tree Level Order Traversal
Problem Description
You are given the root of a binary tree. Your task is to return the level order traversal of the tree's node values. This means you need to traverse the tree level by level, from left to right at each level.
In a level order traversal:
- Start at the root (level 0)
- Visit all nodes at the current level from left to right
- Move to the next level and repeat
- Continue until all levels have been visited
The output should be a list of lists, where each inner list contains all the node values at a particular level of the tree.
For example, if you have a binary tree like:
3 / \ 9 20 / \ 15 7
The level order traversal would be: [[3], [9, 20], [15, 7]]
- Level 0:
[3]
- Level 1:
[9, 20]
- Level 2:
[15, 7]
If the tree is empty (root is None
), return an empty list.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph (specifically, a connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree.
DFS?
- Not quite: While DFS is a valid approach for tree traversal, the problem specifically asks for level-order traversal (visiting nodes level by level from left to right). This is a classic breadth-first traversal pattern, not depth-first.
Let me reconsider the flowchart path. Since we need to traverse level by level:
Is it a graph?
- Yes: A binary tree is a graph structure.
Is it a tree?
- No: While it IS a tree, the specific requirement of level-order traversal suggests we should consider other graph traversal approaches beyond the typical tree DFS.
Is the problem related to directed acyclic graphs?
- No: This is about traversal order, not DAG-specific problems.
Is the problem related to shortest paths?
- No: We're not finding shortest paths, we're traversing all nodes level by level.
Does the problem involve connectivity?
- No: All nodes in a tree are connected by definition; this isn't about finding connected components.
Does the problem have small constraints?
- No: Binary trees can be large, and we need an efficient algorithm.
BFS?
- Yes: BFS (Breadth-First Search) is the perfect fit for level-order traversal, as it naturally processes nodes level by level.
Conclusion: The flowchart leads us to BFS (Breadth-First Search) as the optimal approach for binary tree level-order traversal. BFS uses a queue to process nodes level by level, which perfectly matches the requirement of visiting all nodes at depth d before visiting any node at depth d+1.
Intuition
When we need to traverse a tree level by level, we need a way to keep track of which nodes belong to the same level. Think about how water fills a container - it fills completely at one level before moving to the next level. Similarly, we want to visit all nodes at depth 0, then all nodes at depth 1, and so on.
The key insight is that BFS naturally gives us this level-by-level behavior through its use of a queue. Here's why:
-
Queue maintains order: When we use a queue (FIFO - First In, First Out), nodes are processed in the exact order they were discovered. This means all nodes at level
n
will be processed before any node at leveln+1
. -
Level separation technique: The clever part of the solution is knowing how many nodes belong to the current level. When we start processing a level, we can record the queue's size - this tells us exactly how many nodes are in the current level. We then process exactly that many nodes before moving to the next level.
-
Children go to the back: As we process each node in the current level, we add its children to the back of the queue. These children (which belong to the next level) will only be processed after all nodes in the current level are done.
The algorithm flow becomes:
- Start with root in queue
- While queue is not empty:
- Get current queue size (this is the number of nodes in current level)
- Process exactly that many nodes, collecting their values
- For each processed node, add its children to the queue
- After processing all nodes of current level, we have one complete level's values
This approach guarantees that we visit nodes level by level, left to right, because we process nodes in the order they enter the queue, and sibling nodes enter the queue in left-to-right order.
Learn more about Tree, Breadth-First Search and Binary Tree patterns.
Solution Approach
Following the BFS approach identified through our analysis, let's implement the level-order traversal using a queue data structure.
Implementation Steps:
-
Handle edge case: First check if the root is
None
. If it is, return an empty list since there's nothing to traverse. -
Initialize data structures:
- Create an empty list
ans
to store the final result (list of lists) - Initialize a queue
q
with the root node usingdeque([root])
- Create an empty list
-
Process levels iteratively: While the queue is not empty:
- Create a temporary list
t
to store all node values at the current level - Get the current queue size using
len(q)
- this tells us exactly how many nodes are in the current level - Process exactly that many nodes:
- Remove a node from the front of the queue using
q.popleft()
- Add the node's value to the temporary list
t.append(node.val)
- Add the node's children to the back of the queue:
- If
node.left
exists, append it to the queue - If
node.right
exists, append it to the queue
- If
- Remove a node from the front of the queue using
- After processing all nodes at the current level, add the temporary list
t
to our answer list
- Create a temporary list
-
Return the result: After the queue is empty (all levels processed), return
ans
Why this works:
- The
for _ in range(len(q))
loop ensures we only process nodes that were in the queue at the start of each level iteration - Any children added during this iteration won't be processed until the next level iteration
- Using
deque
andpopleft()
gives us O(1) operations for both adding and removing from the queue - The left-to-right order is preserved because we always check and add the left child before the right child
Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once
Space Complexity: O(w) where w is the maximum width of the tree (for the queue), which in the worst case can be O(n/2) for a complete binary tree
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with a small binary tree:
1 / \ 2 3 / \ 4 5
Initial State:
ans = []
(empty result list)q = deque([1])
(queue contains root)
Level 0 Processing:
- Queue size = 1 (one node at this level)
- Create temporary list:
t = []
- Process 1 node:
- Dequeue node 1:
t = [1]
- Add children:
q = deque([2, 3])
- Dequeue node 1:
- Add level to result:
ans = [[1]]
Level 1 Processing:
- Queue size = 2 (two nodes at this level)
- Create temporary list:
t = []
- Process 2 nodes:
- Node 1: Dequeue node 2:
t = [2]
- Add left child 4:
q = deque([3, 4])
- Add left child 4:
- Node 2: Dequeue node 3:
t = [2, 3]
- Add right child 5:
q = deque([4, 5])
- Add right child 5:
- Node 1: Dequeue node 2:
- Add level to result:
ans = [[1], [2, 3]]
Level 2 Processing:
- Queue size = 2 (two nodes at this level)
- Create temporary list:
t = []
- Process 2 nodes:
- Node 1: Dequeue node 4:
t = [4]
- No children to add
- Node 2: Dequeue node 5:
t = [4, 5]
- No children to add
- Node 1: Dequeue node 4:
- Add level to result:
ans = [[1], [2, 3], [4, 5]]
Termination:
- Queue is empty, return
ans = [[1], [2, 3], [4, 5]]
The key insight: by capturing the queue size at the start of each level, we know exactly how many nodes to process before moving to the next level, even as we're adding new nodes (children) to the queue.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from collections import deque
9from typing import List, Optional
10
11class Solution:
12 def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
13 """
14 Performs level-order traversal (BFS) of a binary tree.
15 Returns a list of lists where each inner list contains values at that level.
16
17 Args:
18 root: The root node of the binary tree
19
20 Returns:
21 A list of lists containing node values grouped by level
22 """
23 result = []
24
25 # Handle empty tree case
26 if root is None:
27 return result
28
29 # Initialize queue with root node for BFS
30 queue = deque([root])
31
32 # Process tree level by level
33 while queue:
34 current_level = []
35 level_size = len(queue)
36
37 # Process all nodes at the current level
38 for _ in range(level_size):
39 # Remove node from front of queue
40 node = queue.popleft()
41
42 # Add node value to current level list
43 current_level.append(node.val)
44
45 # Add children to queue for next level processing
46 if node.left:
47 queue.append(node.left)
48 if node.right:
49 queue.append(node.right)
50
51 # Add current level to result
52 result.append(current_level)
53
54 return result
55
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Performs level-order traversal of a binary tree.
19 * Returns a list where each element is a list containing values of nodes at that level.
20 *
21 * @param root The root node of the binary tree
22 * @return A list of lists containing node values grouped by level
23 */
24 public List<List<Integer>> levelOrder(TreeNode root) {
25 // Initialize the result list to store all levels
26 List<List<Integer>> result = new ArrayList<>();
27
28 // Handle edge case: empty tree
29 if (root == null) {
30 return result;
31 }
32
33 // Initialize queue for BFS traversal
34 Deque<TreeNode> queue = new ArrayDeque<>();
35 queue.offer(root);
36
37 // Process tree level by level
38 while (!queue.isEmpty()) {
39 // List to store current level's node values
40 List<Integer> currentLevel = new ArrayList<>();
41
42 // Get the number of nodes at current level
43 int levelSize = queue.size();
44
45 // Process all nodes at current level
46 for (int i = 0; i < levelSize; i++) {
47 // Remove and process the front node
48 TreeNode currentNode = queue.poll();
49 currentLevel.add(currentNode.val);
50
51 // Add left child to queue for next level processing
52 if (currentNode.left != null) {
53 queue.offer(currentNode.left);
54 }
55
56 // Add right child to queue for next level processing
57 if (currentNode.right != null) {
58 queue.offer(currentNode.right);
59 }
60 }
61
62 // Add current level to the result
63 result.add(currentLevel);
64 }
65
66 return result;
67 }
68}
69
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 vector<vector<int>> levelOrder(TreeNode* root) {
15 vector<vector<int>> result;
16
17 // Handle empty tree case
18 if (!root) {
19 return result;
20 }
21
22 // Initialize queue with root node for BFS traversal
23 queue<TreeNode*> nodeQueue;
24 nodeQueue.push(root);
25
26 // Process nodes level by level
27 while (!nodeQueue.empty()) {
28 vector<int> currentLevel;
29 int levelSize = nodeQueue.size();
30
31 // Process all nodes at the current level
32 for (int i = 0; i < levelSize; ++i) {
33 // Get and remove the front node from queue
34 TreeNode* currentNode = nodeQueue.front();
35 nodeQueue.pop();
36
37 // Add current node's value to current level result
38 currentLevel.push_back(currentNode->val);
39
40 // Add left child to queue for next level processing
41 if (currentNode->left) {
42 nodeQueue.push(currentNode->left);
43 }
44
45 // Add right child to queue for next level processing
46 if (currentNode->right) {
47 nodeQueue.push(currentNode->right);
48 }
49 }
50
51 // Add current level's values to final result
52 result.push_back(currentLevel);
53 }
54
55 return result;
56 }
57};
58
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Performs level-order traversal of a binary tree (BFS)
17 * Returns a 2D array where each sub-array contains node values at that level
18 * @param root - The root node of the binary tree
19 * @returns A 2D array of node values grouped by level
20 */
21function levelOrder(root: TreeNode | null): number[][] {
22 // Result array to store node values by level
23 const result: number[][] = [];
24
25 // Handle empty tree case
26 if (!root) {
27 return result;
28 }
29
30 // Initialize queue with root node for BFS traversal
31 const currentLevelNodes: TreeNode[] = [root];
32
33 // Process nodes level by level
34 while (currentLevelNodes.length > 0) {
35 // Array to store values of nodes at current level
36 const currentLevelValues: number[] = [];
37
38 // Array to store nodes for the next level
39 const nextLevelNodes: TreeNode[] = [];
40
41 // Process all nodes at current level
42 for (const node of currentLevelNodes) {
43 // Add current node's value to level array
44 currentLevelValues.push(node.val);
45
46 // Add left child to next level if exists
47 if (node.left) {
48 nextLevelNodes.push(node.left);
49 }
50
51 // Add right child to next level if exists
52 if (node.right) {
53 nextLevelNodes.push(node.right);
54 }
55 }
56
57 // Add current level values to result
58 result.push(currentLevelValues);
59
60 // Replace current level nodes with next level nodes
61 currentLevelNodes.splice(0, currentLevelNodes.length, ...nextLevelNodes);
62 }
63
64 return result;
65}
66
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a breadth-first traversal of the binary tree using a queue. Each node in the tree is visited exactly once - it's added to the queue once when discovered and removed from the queue once for processing. For each node, we perform constant time operations: popping from the queue (O(1)
), appending the value to the temporary list (O(1)
), and potentially adding left and right children to the queue (O(1)
each). Since we visit all n
nodes exactly once and perform constant time operations for each node, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of two main components:
- Queue space: In the worst case, the queue can contain all nodes at a single level. For a complete binary tree, the maximum width occurs at the last level, which can have up to
n/2
nodes. Thus, the queue space isO(n)
. - Output space: The
ans
list stores alln
node values organized by levels. The temporary listt
at each level also contributes to space usage, but its maximum size is bounded by the maximum width of the tree. Overall, the output requiresO(n)
space.
Therefore, the total space complexity is O(n)
, where n
is the number of nodes in the binary tree.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Tracking Level Boundaries Correctly
One of the most common mistakes is failing to properly separate nodes by their levels. Without tracking how many nodes belong to each level, you might end up mixing nodes from different levels together.
Incorrect approach:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
result = []
queue = deque([root])
current_level = []
while queue:
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
# Wrong: This doesn't know when a level ends
if some_condition: # What condition?
result.append(current_level)
current_level = []
return result
Solution: Always capture the queue size at the beginning of each level iteration. This tells you exactly how many nodes to process before moving to the next level:
while queue:
level_size = len(queue) # Capture current level size
current_level = []
for _ in range(level_size): # Process exactly this many nodes
node = queue.popleft()
current_level.append(node.val)
# Add children for next level
2. Using a List Instead of Deque
Using a regular Python list with pop(0)
instead of deque
with popleft()
creates a performance issue:
Inefficient approach:
queue = [root] # Regular list while queue: node = queue.pop(0) # O(n) operation!
Solution: Always use collections.deque
for queue operations:
from collections import deque queue = deque([root]) node = queue.popleft() # O(1) operation
The pop(0)
operation on a list requires shifting all remaining elements, making it O(n), while deque.popleft()
is O(1).
3. Processing Children in Wrong Order
Adding children in the wrong order will produce incorrect left-to-right traversal:
Wrong order:
if node.right: # Right child first - incorrect! queue.append(node.right) if node.left: queue.append(node.left)
Solution: Always add the left child before the right child:
if node.left: queue.append(node.left) if node.right: queue.append(node.right)
4. Modifying Queue Size During Level Iteration
A subtle bug occurs when you don't capture the level size before the loop:
Problematic approach:
while queue:
current_level = []
# Wrong: len(queue) changes as we add children!
for _ in range(len(queue)):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left) # This changes len(queue)!
While this might seem to work, it's conceptually clearer and less error-prone to capture the size first:
level_size = len(queue) # Capture once
for _ in range(level_size): # Use captured value
# Process nodes...
5. Forgetting to Handle Empty Tree
Not checking if the root is None
will cause an error when trying to access properties of a null node:
Missing edge case:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
result = []
queue = deque([root]) # If root is None, this adds None to queue
while queue:
node = queue.popleft()
result.append(node.val) # Error if node is None!
Solution: Always check for empty tree first:
if root is None: return []
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!