Skip to main content

Level Order Traversal of Binary Trees

Complete Java solution for level order traversal with BFS approach, including edge case handling and time complexity analysis.

Level order traversal processes binary tree nodes level by level from left to right. This fundamental tree algorithm uses a queue-based BFS approach to systematically visit all nodes at each depth level before moving to the next.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. What is the expected input format - is it a TreeNode class or array representation of a binary tree?
  2. Should we return the result as a list of lists, or can we return it as a flat list with level separators?
  3. Are we allowed to modify the input tree structure during traversal?
  4. What is the maximum expected size of the tree - does it impact our approach choice?
  5. Should we optimize for time or space complexity in the follow-up?

Restate the pattern or problem type in your own words: Level order traversal (also known as breadth-first traversal) is a tree traversal algorithm that visits nodes level by level from left to right. It processes all nodes at the current depth before moving to nodes at the next depth level.

Clarify the signal words and constraint shape that point to it:

  • "Level by level" or "depth by depth" processing
  • "Left to right" ordering within each level
  • Tree or graph structure where we need to process nodes in breadth-first fashion
  • Problems asking for "top-down" or "horizontal" organization of data

Note any assumptions you are making:

  • The input is a valid binary tree (or graph) with proper parent-child relationships
  • We can use a queue data structure for the traversal
  • The tree nodes have standard properties (left, right pointers or adjacency lists)
  • We're not concerned with maintaining the exact tree structure during traversal

Identify whether the pattern is best suited for static, streaming, or online scenarios: This pattern is primarily designed for static scenarios where the entire tree structure is available upfront. It's not naturally suited for streaming data where nodes arrive dynamically, though it can be adapted for such cases with additional complexity. The pattern works well when we need to process all nodes in a predictable hierarchical order.

2. When to Use This Pattern

Signal words and constraint shapes in a problem statement that indicate this pattern:

  • "Level order traversal" or "breadth-first traversal"
  • "Print tree by levels" or "nodes at same depth"
  • "Return nodes grouped by level"
  • Binary tree or N-ary tree input
  • Need to process nodes from left to right within each level

Why this pattern fits better than the most common alternatives: The level order traversal pattern is specifically designed for processing tree nodes in breadth-first manner, which is fundamentally different from depth-first approaches like pre-order, in-order, or post-order traversals. Unlike DFS which goes deep into one branch before exploring siblings, level order traversal ensures all nodes at depth k are processed before any nodes at depth k+1. This makes it ideal when the problem requires level-by-level processing rather than hierarchical exploration.

Conditions where a variation or related pattern applies instead:

  • For simple tree traversal without level grouping, use DFS patterns (pre-order/in-order/post-order)
  • For processing nodes in a specific depth-first order, use DFS with stack-based approaches
  • For finding shortest paths in unweighted graphs, use BFS (which is the same as level order traversal for trees)
  • For processing nodes in reverse level order, use level order with reverse collection

The core insight that makes this pattern work: The key insight is that we can leverage a queue to maintain the FIFO (First In, First Out) property necessary for breadth-first processing. By processing nodes level by level and enqueuing their children before moving to the next node in the current level, we naturally group nodes by their depth in the tree. This creates a clean separation between levels that can be easily captured and returned as separate collections.

The pattern works because:

  1. Queue maintains the order of nodes to be processed
  2. Processing nodes in batches by current queue size ensures level grouping
  3. Children of processed nodes are enqueued for the next level processing
  4. This creates a natural boundary between levels that can be captured and returned

3. Approach Evolution

Interview communication script:

"My first instinct is to do a depth-first search approach where I traverse the tree and keep track of levels, but that's O(n²) in worst case because I'd have to repeatedly check and update level tracking. Let me think about how to optimize this...

I notice that if I maintain a queue structure and process nodes level by level, I can reduce it to O(n) time complexity. The key insight is that I need to process all nodes at the current level before moving to the next level. This naturally suggests a breadth-first search pattern where I can group nodes by their levels.

Let me think about the invariants here. If I use a queue to process nodes, at each step I should have all nodes from the current level in the queue. When I process them, I should collect their children for the next level. This way I can naturally group nodes by levels without needing to track depth explicitly.

The optimization comes from realizing that instead of doing recursive DFS and maintaining level information, I can do iterative BFS with a queue and process nodes in batches by their levels. This gives me clean separation of levels without extra tracking overhead."

Brute-force approach and why it is not ideal:

The brute force approach would be to perform a depth-first search (DFS) and track the level of each node. For each node visited, we'd store its value with its corresponding level in a map or list. Then we'd iterate through levels to collect results.

This approach is inefficient because:

  • We need to traverse the tree multiple times to group nodes by levels
  • The level tracking logic adds complexity and potential for bugs
  • We have to maintain additional data structures to track levels
  • Time complexity becomes O(n²) in worst case when we have to repeatedly look up level information

Improved approach and the key optimization:

The improved approach uses a breadth-first search (BFS) with a queue to naturally process nodes level by level. The key optimization is that we can use the queue's natural ordering to group nodes by their levels without maintaining explicit level tracking.

Instead of storing nodes with their levels separately, we process all nodes at the current level before moving to the next level. At each iteration of our BFS loop, we process all nodes currently in the queue (which represents one complete level), collect their children for the next level, and add the current level's values to our result.

Final approach with step-by-step reasoning:

  1. Start with a queue containing only the root node
  2. While the queue is not empty:
    • Process all nodes currently in the queue (this represents one complete level)
    • Collect their values into a list for that level
    • Add all non-null children of current nodes to the queue for the next level
  3. Continue until no more levels exist

This approach works because:

  • The queue naturally maintains nodes in level order due to FIFO processing
  • At each iteration, we process exactly one level's worth of nodes
  • We don't need to track levels explicitly - the queue structure handles it
  • Each node is processed exactly once, giving us O(n) time complexity

Important invariants that must hold:

  1. At the start of each iteration, the queue contains only nodes from the current level to be processed
  2. After processing all nodes in the current queue, the queue contains only children from the next level
  3. Each node is processed exactly once in the entire traversal
  4. The order of nodes within each level matches their left-to-right appearance in the tree

4. Data Structure Choice

For level order traversal, I need to carefully consider the data structures that will enable efficient processing of nodes level by level.

The primary data structure is a Queue (specifically LinkedList or ArrayDeque) to maintain the FIFO order required for breadth-first processing. Each level's nodes must be processed before moving to the next level, making a queue the natural choice.

Why Queue is essential:

  • Maintains the order of nodes to be processed at each level
  • Enables O(1) insertion and removal operations
  • Provides the fundamental mechanism for breadth-first exploration

Why other structures are suboptimal:

  • Stack: Would cause depth-first traversal instead of breadth-first
  • Set/HashSet: Doesn't maintain order and would require additional tracking logic
  • Array/List: Would need manual index management for level boundaries

Helper structures: I'll use a List of Lists to organize the output by levels. For each level, I'll collect all node values before moving to the next level.

Space considerations:

  • Queue space: O(w) where w is maximum width of the tree (at most n/2 nodes at the widest level)
  • Output space: O(n) for storing all node values
  • Overall space complexity: O(n)

The queue serves as the primary driver for level-by-level processing, while the output container organizes results by level structure. This combination enables clean separation of concerns: one data structure handles the traversal logic, another handles the result formatting.

// Queue for BFS traversal
Queue<TreeNode> queue = new LinkedList<>();
// List of Lists to store level-by-level results  
List<List<Integer>> result = new ArrayList<>();

5. Algorithm Walkthrough

Let me trace through the level order traversal algorithm using a concrete example:

Example Input: Binary tree with root = [3,9,20,null,null,15,7]

Step-by-step execution:

  1. Initialization:

    • Queue contains root node (value 3)
    • Result list is empty
  2. First Iteration:

    • Dequeue node with value 3
    • Add 3 to result list: [[3]]
    • Enqueue children: 9 (left) and 20 (right)
    • Queue now contains [9, 20]
  3. Second Iteration:

    • Dequeue node with value 9
    • Add 9 to current level: [[3], [9]]
    • Enqueue children: null (left) and null (right) - no action
    • Queue now contains [20]
  4. Third Iteration:

    • Dequeue node with value 20
    • Add 20 to current level: [[3], [9], [20]]
    • Enqueue children: 15 (left) and 7 (right)
    • Queue now contains [15, 7]
  5. Fourth Iteration:

    • Dequeue node with value 15
    • Add 15 to current level: [[3], [9], [20], [15]]
    • Enqueue children: null (left) and null (right) - no action
  6. Fifth Iteration:

    • Dequeue node with value 7
    • Add 7 to current level: [[3], [9], [20], [15], [7]]
    • Enqueue children: null (left) and null (right) - no action
  7. Termination:

    • Queue is empty, return result: [[3], [9, 20], [15, 7]]

Key handling points:

  • The algorithm processes nodes level by level using FIFO queue properties
  • Each iteration handles exactly one level's worth of nodes
  • Null children are safely ignored (no need to add them to queue)
  • The queue naturally maintains the breadth-first processing order

The algorithm correctly captures all nodes in their respective levels, maintaining proper left-to-right ordering within each level.

6. Java Solution

import java.util.*;

// Definition for a binary tree node
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public class LevelOrderTraversal {
    
    /**
     * Returns the level order traversal of a binary tree's nodes' values.
     * Uses BFS with a queue to process nodes level by level.
     * 
     * @param root the root of the binary tree
     * @return list of lists containing node values level by level
     */
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        
        // Handle empty tree case
        if (root == null) {
            return result;
        }
        
        // BFS using queue - process nodes level by level
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        
        while (!queue.isEmpty()) {
            int levelSize = queue.size();  // Number of nodes at current level
            List<Integer> currentLevel = new ArrayList<>();
            
            // Process all nodes at the current level
            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                currentLevel.add(node.val);
                
                // Add children to queue for next level processing
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            
            result.add(currentLevel);
        }
        
        return result;
    }
}

The solution implements a standard BFS approach using a queue to traverse the binary tree level by level. For each level, we process all nodes currently in the queue before moving to the next level, ensuring proper ordering. The key insight is using the queue size at each iteration to determine how many nodes belong to the current level.

7. Complexity Analysis

Time Complexity: O(N) where N is the number of nodes in the binary tree. Each node is visited exactly once during the level order traversal, and processing each node takes constant time.

Space Complexity: O(W) where W is the maximum width of the binary tree. In the worst case, the queue will contain all nodes at the widest level of the tree. For a complete binary tree, the maximum width is approximately N/2, making space complexity O(N) in the worst case. However, for a balanced tree, it's more accurately described as O(W) where W is the maximum width.

The time complexity is optimal because we must visit each node at least once to produce a valid level order traversal. The space complexity is also near-optimal since we need to store nodes at each level to maintain the traversal order.

The solution is optimal because:

  1. We process each node exactly once
  2. We use the minimum necessary space to maintain the traversal order
  3. No additional preprocessing or indexing is required

The O(N) time bound holds because:

  • Each node is enqueued exactly once
  • Each node is dequeued exactly once
  • Dequeue and enqueue operations are O(1)
  • Processing each node's value is O(1)

The O(W) space bound holds because:

  • At any point, the queue contains at most all nodes at one level
  • The maximum number of nodes at any single level is W (the width)
  • In the worst case, W = N/2 for a complete binary tree

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty tree: Handle null root gracefully to avoid NullPointerException
  • Single node: Verify that a single-node tree returns correct result [root.val]
  • Complete binary tree vs. skewed tree: Ensure algorithm works for both balanced and unbalanced structures

Duplicate Values and Repeated States

  • Duplicate node values: The traversal should process nodes in order regardless of duplicate values
  • Same value at different levels: Values can appear multiple times but must be grouped by level

Boundary Conditions

  • Null nodes in middle: When processing a node's children, ensure null checks prevent runtime errors
  • Level 0 vs. level 1: Verify proper grouping of nodes at different depths starting from root (level 0)
  • Large tree depth: Handle trees with many levels without stack overflow or memory issues

Common Implementation Mistakes

  • Incorrect queue usage: Using Stack instead of Queue will cause depth-first rather than breadth-first traversal
  • Not tracking levels properly: Mismanaging level boundaries leads to wrong grouping of nodes
  • Processing children in wrong order: Adding right child before left child changes the traversal order
  • Modifying input structure: Mutating original tree nodes during traversal (e.g., setting left = null) affects correctness
  • Missing null checks: Not checking if current.left or current.right is null before adding to queue

Memory Management Pitfalls

  • Queue size explosion: For wide trees, queue can grow very large; ensure memory usage is proportional to tree width
  • Not clearing intermediate collections: Retaining references unnecessarily can cause memory leaks in production systems

The key to avoiding these pitfalls is thorough testing with edge cases and maintaining clean separation between traversal logic and tree mutation.

9. Alternative Approaches

There are several alternative approaches to level order traversal, each with different trade-offs:

1. Recursive DFS with Level Tracking Instead of using a queue, we can use recursive depth-first search while tracking the current level. For each node, we add its value to the appropriate level list in our result structure.

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    dfs(root, 0, result);
    return result;
}

private void dfs(TreeNode node, int level, List<List<Integer>> result) {
    if (node == null) return;
    
    if (result.size() <= level) {
        result.add(new ArrayList<>());
    }
    
    result.get(level).add(node.val);
    dfs(node.left, level + 1, result);
    dfs(node.right, level + 1, result);
}

Trade-offs:

  • Time complexity: O(n) - still optimal
  • Space complexity: O(h) where h is height (due to recursion stack)
  • Pros: More intuitive for some developers
  • Cons: Uses call stack space, less efficient for very deep trees

2. Two-Queue Approach Use two separate queues to track nodes at each level, alternating between them.

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue<TreeNode> currentLevel = new LinkedList<>();
    Queue<TreeNode> nextLevel = new LinkedList<>();
    
    currentLevel.offer(root);
    
    while (!currentLevel.isEmpty()) {
        List<Integer> levelValues = new ArrayList<>();
        
        while (!currentLevel.isEmpty()) {
            TreeNode node = currentLevel.poll();
            levelValues.add(node.val);
            
            if (node.left != null) nextLevel.offer(node.left);
            if (node.right != null) nextLevel.offer(node.right);
        }
        
        result.add(levelValues);
        Queue<TreeNode> temp = currentLevel;
        currentLevel = nextLevel;
        nextLevel = temp;
    }
    
    return result;
}

Trade-offs:

  • Time complexity: O(n) - optimal
  • Space complexity: O(w) where w is maximum width (more space than single queue)
  • Pros: Clear separation of levels
  • Cons: More complex implementation, extra space usage

3. BFS with Null Marker Use a special marker (like null) to indicate level boundaries in the queue.

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    queue.offer(null); // marker for level end
    
    List<Integer> currentLevel = new ArrayList<>();
    
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        
        if (node == null) {
            result.add(currentLevel);
            currentLevel = new ArrayList<>();
            
            if (!queue.isEmpty()) {
                queue.offer(null); // add new level marker
            }
        } else {
            currentLevel.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    
    return result;
}

Trade-offs:

  • Time complexity: O(n) - optimal
  • Space complexity: O(w) - same as standard BFS but with extra null markers
  • Pros: No need for separate level tracking
  • Cons: More complex logic, extra null objects in queue

The original approach using a single queue with size tracking is generally preferred because it's clean, efficient, and easy to understand. The recursive DFS approach works well for problems where we want to process levels in a different order or when we're more comfortable with recursion than iterative queue-based solutions.

10. Test Cases

import java.util.*;

public class LevelOrderTraversalTestCases {
    
    // Test case 1: Basic binary tree
    //       3
    //      / \
    //     9   20
    //        /  \
    //       15   7
    public void testBasicTree() {
        TreeNode root = new TreeNode(3);
        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);
        
        List<List<Integer>> expected = Arrays.asList(
            Arrays.asList(3),
            Arrays.asList(9, 20),
            Arrays.asList(15, 7)
        );
        
        // Expected: [[3], [9, 20], [15, 7]]
    }
    
    // Test case 2: Single node tree
    public void testSingleNode() {
        TreeNode root = new TreeNode(1);
        
        List<List<Integer>> expected = Arrays.asList(
            Arrays.asList(1)
        );
        
        // Expected: [[1]]
    }
    
    // Test case 3: Empty tree
    public void testEmptyTree() {
        TreeNode root = null;
        
        List<List<Integer>> expected = new ArrayList<>();
        
        // Expected: []
    }
    
    // Test case 4: Skewed tree (all left children)
    //     1
    //    /
    //   2
    //  /
    // 3
    public void testSkewedTree() {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.left.left = new TreeNode(3);
        
        List<List<Integer>> expected = Arrays.asList(
            Arrays.asList(1),
            Arrays.asList(2),
            Arrays.asList(3)
        );
        
        // Expected: [[1], [2], [3]]
    }
    
    // Test case 5: Complete binary tree
    //       1
    //      / \
    //     2   3
    //    / \ / \
    //   4  5 6  7
    public void testCompleteTree() {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);
        root.right.left = new TreeNode(6);
        root.right.right = new TreeNode(7);
        
        List<List<Integer>> expected = Arrays.asList(
            Arrays.asList(1),
            Arrays.asList(2, 3),
            Arrays.asList(4, 5, 6, 7)
        );
        
        // Expected: [[1], [2, 3], [4, 5, 6, 7]]
    }
    
    // Test case 6: Tree with null nodes (should not include nulls in output)
    public void testTreeWithNulls() {
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = null;  // This null should not appear in result
        root.right.left = null;
        root.right.right = new TreeNode(5);
        
        List<List<Integer>> expected = Arrays.asList(
            Arrays.asList(1),
            Arrays.asList(2, 3),
            Arrays.asList(4, 5)
        );
        
        // Expected: [[1], [2, 3], [4, 5]]
    }
    
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        
        TreeNode(int val) {
            this.val = val;
            this.left = null;
            this.right = null;
        }
    }
}

Sample Test Cases Summary:

  1. Basic Tree: Standard binary tree with 3 levels
  2. Single Node: Edge case with only root node
  3. Empty Tree: Null root handling
  4. Skewed Tree: All left children creating deep tree
  5. Complete Tree: Perfectly balanced binary tree
  6. Tree with Nulls: Handling missing nodes in the tree structure

Each test case covers different structural scenarios to ensure robustness of the level order traversal implementation. The key is that we're testing both structural diversity and edge case handling, particularly around null nodes which should not appear in the final output list but are valid in tree structure.

11. Final Summary

The core idea is to process a binary tree level by level using a queue-based BFS approach, where each level's nodes are processed together before moving to the next level. This solution works efficiently by leveraging the queue's FIFO property to maintain the correct traversal order, ensuring that all nodes at depth d are processed before any nodes at depth d+1. The algorithm is optimal with O(n) time and space complexity, where n is the number of nodes, making it the most efficient approach for level-order traversal of binary trees.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Problems

    Binary Tree Level Order Traversal Algorithm

    Complete Java solution for binary tree level order traversal with BFS approach and time complexity analysis.

  • DSA Problems

    Binary Tree Right Side View Algorithm Solution

    Complete Java implementation for binary tree right side view problem with BFS and DFS approaches, including time/space complexity analysis.

  • DSA Problems

    Lowest Common Ancestor of a Binary Tree

    Find the lowest common ancestor of two nodes in a binary tree with an efficient recursive approach.

  • DSA Problems

    Diameter of Binary Tree Solution in Java

    Complete Java implementation for finding the diameter of a binary tree with optimal time and space complexity analysis.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.