Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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 level n+1.

  2. 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.

  3. 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:

  1. Handle edge case: First check if the root is None. If it is, return an empty list since there's nothing to traverse.

  2. Initialize data structures:

    • Create an empty list ans to store the final result (list of lists)
    • Initialize a queue q with the root node using deque([root])
  3. 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
    • After processing all nodes at the current level, add the temporary list t to our answer list
  4. 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 and popleft() 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 Evaluator

Example 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])
  • 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])
    • Node 2: Dequeue node 3: t = [2, 3]
      • Add right child 5: q = deque([4, 5])
  • 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
  • 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:

  1. 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 is O(n).
  2. Output space: The ans list stores all n node values organized by levels. The temporary list t at each level also contributes to space usage, but its maximum size is bounded by the maximum width of the tree. Overall, the output requires O(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 []
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More