Skip to main content

Breadth-First Search Queue Patterns in Java

Comprehensive guide to BFS queue patterns including level-order traversal, shortest path finding, and multi-source exploration with practical Java implementations.

Breadth-First Search (BFS) queue patterns form the foundation of many graph and tree traversal problems, offering systematic approaches to explore nodes level by level while maintaining optimal time complexity for shortest path calculations.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. What's the input size and constraints? Are we dealing with small graphs (dozens of nodes) or large-scale networks that might require optimizations?
  2. Can duplicates appear in the input? For example, if we're given a graph with duplicate edges or nodes, how should we handle them?
  3. What's the expected return format? Should I return a list of nodes in BFS order, or do I need to track distances or parent relationships?
  4. Can the input structure be mutated? For example, if I'm given a graph representation, am I allowed to modify it during traversal?
  5. What's the target complexity for follow-ups? Is O(n + m) time and space acceptable, or should I aim for a more optimized solution?

Restatement of the pattern:

The BFS queue pattern is a graph traversal technique that systematically explores nodes level-by-level using a queue data structure. It guarantees that all nodes at distance k from the starting node are visited before any nodes at distance k+1, making it ideal for finding shortest paths in unweighted graphs or exploring all reachable nodes from a given starting point.

Signal words and constraint shape:

Key indicators include:

  • "Shortest path in unweighted graph"
  • "Level-by-level traversal"
  • "All nodes reachable from a source"
  • "Breadth-first exploration"
  • "Queue-based processing"

The pattern typically appears in problems involving:

  • Tree or graph traversal where order matters
  • Finding the minimum number of steps between nodes
  • Level-order processing (like printing tree levels)
  • Connectivity problems in graphs

Assumptions:

  1. The input is a connected or potentially disconnected graph (or tree)
  2. Nodes can be represented as integers or objects with identifiers
  3. The graph is undirected or directed, but the traversal logic adapts accordingly
  4. BFS queue pattern assumes we're processing nodes in a specific order, not just any traversal

Scenario suitability:

This pattern is best suited for static or batch scenarios where we have full knowledge of the graph structure upfront. It's less appropriate for streaming or online problems where nodes arrive dynamically and we must maintain the traversal state in real-time. For streaming, a more adaptive pattern like incremental graph processing or sliding window approaches would be better suited.

The BFS queue pattern is a foundational tool for problems requiring level-by-level exploration or shortest-path computation in unweighted graphs.

2. When to Use This Pattern

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

  • "Shortest path" or "minimum steps" requirements
  • "Level-by-level" processing of nodes
  • Problems involving tree/graph traversal where order matters
  • "All nodes at distance K from source" queries
  • Problems requiring exploration of all reachable states in a systematic way

Why this pattern fits better than the most common alternatives: BFS is superior to DFS for shortest path problems because it explores nodes level-by-level, guaranteeing the first time a node is reached is via the shortest path. Unlike DFS which might go deep into one branch before backtracking, BFS systematically expands all possibilities at the current depth before moving to the next level.

Conditions where a variation or related pattern applies instead:

  • When only one path matters (DFS), or when the graph is a tree with no cycles, BFS becomes unnecessary overhead
  • For single-source shortest paths in weighted graphs, Dijkstra's algorithm is more appropriate than BFS
  • When the problem requires maintaining state across levels (like finding minimum operations), BFS with queue is optimal

The core insight that makes this pattern work: BFS naturally processes nodes in order of their distance from the starting point, making it ideal for problems where we need to explore all possibilities at each "step" or "level" before advancing. This systematic exploration ensures optimal solutions for unweighted graphs and level-based problems.

The key advantage is that BFS guarantees the first time we reach a target node is with the minimum number of steps, which is not guaranteed in other traversal methods.

3. Approach Evolution

Interview communication script:

"My first instinct is to think about this as a graph traversal problem where we need to explore all nodes at the current depth before moving to the next level. That immediately signals BFS because we want to process nodes level by level, which is exactly what BFS does with a queue.

But let me think about the brute force approach first - if I were to do a simple DFS, I'd end up exploring paths that might not be optimal, and I'd have to keep track of multiple states. That's inefficient because I'm not leveraging the level-by-level nature of the problem.

The key insight is that if I use BFS with a queue, I can guarantee that when I reach a node for the first time, I'm visiting it at the minimum depth. This is because BFS explores nodes in order of their distance from the starting point.

Let me think about how to optimize this. The brute force approach would be to do a full traversal and track the minimum depth, but that's O(n) time in worst case. But since we're doing BFS, we can stop as soon as we find the first target node - that's our answer.

I notice that if I maintain the queue properly and track levels, I can reduce it to O(n) time where n is the number of nodes in the graph. The key invariant here is that I process all nodes at level k before processing any nodes at level k+1. This ensures optimal traversal without redundant exploration.

The optimization comes from the fact that BFS naturally gives us the shortest path in unweighted graphs, so I don't need to worry about finding multiple paths - just the first one that reaches my target."

Brute-force approach and why it is not ideal: The brute force approach would involve exploring all possible paths using DFS or recursive traversal, which could lead to exponential time complexity. This approach doesn't leverage the level-by-level nature of shortest path problems and would require tracking multiple paths simultaneously, making it inefficient.

Improved approach and the key optimization: The improved approach uses BFS with a queue to systematically explore nodes level by level. The key optimization is that we can stop as soon as we find the first occurrence of our target, since BFS guarantees the shortest path in unweighted graphs. This reduces unnecessary exploration and ensures optimal time complexity.

Final approach with step-by-step reasoning:

  1. Initialize a queue with the starting node and its depth (0)
  2. Process nodes level by level using FIFO queue behavior
  3. For each node, check if it's our target - if yes, return the current depth
  4. Add all unvisited neighbors to the queue with incremented depth
  5. Continue until queue is empty or target is found

Important invariants that must hold:

  • Queue always contains nodes to be processed with their current depths
  • Each node is visited at most once (preventing cycles and redundant work)
  • Depth tracking ensures we process nodes in order of increasing distance from start
  • BFS invariant: all nodes at depth k are processed before any nodes at depth k+1

4. Data Structure Choice

For BFS queue patterns, the primary data structure is a Queue to manage the traversal order of nodes. In Java, we typically use LinkedList<T> or ArrayDeque<T> as the queue implementation.

Primary Data Structures Used

  1. Queue (LinkedList/ArrayDeque) - The core BFS structure that maintains the order of nodes to be processed
  2. Set/Boolean Array - Optional auxiliary structure for tracking visited nodes to avoid cycles

Why Each Data Structure Is Needed

  • Queue: BFS requires processing nodes in first-in-first-out order, which is the fundamental behavior of a queue. Each level of the tree/graph must be processed completely before moving to the next level.
  • Set/Visited Array: Prevents revisiting nodes and avoids infinite loops in cyclic graphs, ensuring each node is processed exactly once.

Time and Space Impact

  • Queue: O(n) space complexity where n is the number of nodes, and O(1) amortized time for enqueue/dequeue operations
  • Visited Set: O(n) space complexity to store visited node identifiers, and O(1) average time for lookups

Helper Structures

  • Level tracking: Sometimes we maintain a separate counter or use queue size to track levels when level-by-level processing is required
  • Node wrapper: For complex problems, we might wrap the node with additional metadata (like distance, parent, etc.) in a custom class

The choice between LinkedList and ArrayDeque for the queue is typically ArrayDeque due to better performance characteristics (O(1) operations vs O(n) in worst case for LinkedList), though both are valid.

// Example usage pattern:
Queue<TreeNode> queue = new ArrayDeque<>();
Set<TreeNode> visited = new HashSet<>();

5. Algorithm Walkthrough

Let me trace through the algorithm using a concrete example to show how it works step by step.

Example: rooms = [[2147483647,-1,0,2147483647],[2147483647,2147483647,2147483647,-1],[2147483647,-1,2147483647,-1],[0,-1,2147483647,2147483647]]

Initial state:

  • Queue contains all cells with value 0 (the gates)
  • All other cells have value 2147483647 (empty rooms)

Step-by-step execution:

  1. Initialization:

    • Add all gate positions (0,2) and (3,0) to queue
    • Queue: [(0,2), (3,0)]
  2. First iteration (queue size = 1):

    • Process cell (0,2)
    • Check neighbors: (0,1), (1,2)
    • (0,1) is 2147483647 → update to 1, add to queue
    • (1,2) is 2147483647 → update to 1, add to queue
    • Queue: [(3,0), (0,1), (1,2)]
  3. Second iteration (queue size = 2):

    • Process cell (3,0)
    • Check neighbors: (2,0), (3,1)
    • (2,0) is 2147483647 → update to 1, add to queue
    • (3,1) is -1 → skip
    • Queue: [(0,1), (1,2), (2,0)]
  4. Third iteration (queue size = 3):

    • Process cell (0,1)
    • Check neighbors: (0,0), (0,2)
    • (0,0) is 2147483647 → update to 2, add to queue
    • (0,2) already processed → skip
    • Queue: [(1,2), (2,0), (0,0)]
  5. Continue until queue is empty:

    • Process remaining cells in BFS order
    • Each cell gets updated with minimum distance to nearest gate
    • The algorithm guarantees shortest path due to BFS property

Key insight: By processing cells in BFS order, we ensure that when we update a cell's distance, it's with the minimum possible value since we've already processed all closer cells.

Edge case handling:

  • Walls (-1) are never added to queue
  • Empty rooms (2147483647) are only updated if they're reachable
  • The algorithm naturally handles multiple gates by exploring all simultaneously

The algorithm terminates correctly because:

  1. Each cell is processed at most once
  2. Queue ensures we process cells in order of increasing distance
  3. All reachable empty rooms get their minimum distances computed

6. Java Solution

import java.util.*;

public class BFSQueueSolution {
    
    /**
     * Returns the minimum number of steps to reach target from 1 in BFS traversal
     * where from any number x, we can go to x+1 or 2*x
     */
    public int minStepsToReachTarget(int target) {
        if (target <= 0) return -1;
        if (target == 1) return 0;
        
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> visited = new HashSet<>();
        
        queue.offer(1);
        visited.add(1);
        
        int steps = 0;
        
        while (!queue.isEmpty()) {
            int size = queue.size();
            
            // Process all nodes at current level
            for (int i = 0; i < size; i++) {
                int current = queue.poll();
                
                // Generate next possible states
                int[] nextStates = {current + 1, current * 2};
                
                for (int next : nextStates) {
                    // Skip if out of bounds or already visited
                    if (next > 2 * target || visited.contains(next)) {
                        continue;
                    }
                    
                    // Check if we reached target
                    if (next == target) {
                        return steps + 1;
                    }
                    
                    queue.offer(next);
                    visited.add(next);
                }
            }
            
            steps++;
        }
        
        return -1; // Target not reachable
    }
    
    /**
     * BFS to find shortest path in 2D grid with obstacles
     */
    public int shortestPathBinaryMatrix(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return -1;
        }
        
        int rows = grid.length;
        int cols = grid[0].length;
        
        // Check if start or end is blocked
        if (grid[0][0] == 1 || grid[rows-1][cols-1] == 1) {
            return -1;
        }
        
        if (rows == 1 && cols == 1) {
            return 1;
        }
        
        Queue<int[]> queue = new LinkedList<>();
        boolean[][] visited = new boolean[rows][cols];
        
        // Directions: 8 possible movements (including diagonals)
        int[][] directions = {
            {-1,-1}, {-1,0}, {-1,1},
            {0,-1},           {0,1},
            {1,-1},  {1,0},  {1,1}
        };
        
        queue.offer(new int[]{0, 0});
        visited[0][0] = true;
        
        int pathLength = 1;
        
        while (!queue.isEmpty()) {
            int size = queue.size();
            
            for (int i = 0; i < size; i++) {
                int[] current = queue.poll();
                int row = current[0];
                int col = current[1];
                
                // Check if we reached destination
                if (row == rows - 1 && col == cols - 1) {
                    return pathLength;
                }
                
                // Explore all 8 directions
                for (int[] dir : directions) {
                    int newRow = row + dir[0];
                    int newCol = col + dir[1];
                    
                    // Check bounds and validity
                    if (newRow >= 0 && newRow < rows && 
                        newCol >= 0 && newCol < cols &&
                        !visited[newRow][newCol] && 
                        grid[newRow][newCol] == 0) {
                        
                        visited[newRow][newCol] = true;
                        queue.offer(new int[]{newRow, newCol});
                    }
                }
            }
            
            pathLength++;
        }
        
        return -1; // No path found
    }
    
    /**
     * BFS to find minimum moves for a knight on chessboard
     */
    public int minKnightMoves(int x, int y) {
        if (x == 0 && y == 0) return 0;
        
        // Due to symmetry, we can work with positive coordinates only
        x = Math.abs(x);
        y = Math.abs(y);
        
        // Knight moves: 8 possible moves
        int[][] moves = {{2,1}, {1,2}, {-1,2}, {-2,1}, {-2,-1}, {-1,-2}, {1,-2}, {2,-1}};
        
        Queue<int[]> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        
        queue.offer(new int[]{0, 0});
        visited.add("0,0");
        
        int steps = 0;
        
        while (!queue.isEmpty()) {
            int size = queue.size();
            
            for (int i = 0; i < size; i++) {
                int[] current = queue.poll();
                int currX = current[0];
                int currY = current[1];
                
                if (currX == x && currY == y) {
                    return steps;
                }
                
                // Explore all 8 possible knight moves
                for (int[] move : moves) {
                    int nextX = currX + move[0];
                    int nextY = currY + move[1];
                    String key = nextX + "," + nextY;
                    
                    // Optimization: don't go too far from target
                    if (nextX >= -1 && nextY >= -1 && !visited.contains(key)) {
                        visited.add(key);
                        queue.offer(new int[]{nextX, nextY});
                    }
                }
            }
            
            steps++;
        }
        
        return -1;
    }
}

This solution implements three classic BFS queue patterns:

  1. Simple BFS traversal - For finding minimum steps to reach a target value using two operations (add 1, multiply by 2)
  2. Grid pathfinding - For finding shortest path in a binary matrix with obstacles, using 8-directional movement
  3. Knight moves - For finding minimum moves for a knight on a chessboard with symmetry optimization

Each implementation uses a queue to process nodes level-by-level, maintains visited sets to avoid cycles, and processes all nodes at the current level before moving to the next. The solutions are optimized for both time and space efficiency while handling edge cases appropriately.

7. Complexity Analysis

Time Complexity: O(V + E) where V is the number of nodes and E is the number of edges in the graph.

In a BFS traversal, each node is visited exactly once, and each edge is examined exactly once when considering the neighbors of each node. The queue operations (offer and poll) are O(1), and we process each edge at most once, leading to linear time complexity relative to the graph size.

Space Complexity: O(W + V) where W is the maximum width of the BFS level and V is the number of nodes.

The space complexity comes from two sources:

  1. The queue, which can hold at most W nodes at any given time (where W is the maximum width of the BFS level)
  2. The visited set, which stores at most V nodes

In the worst case for a complete binary tree or similar structures, W could approach V/2, making space complexity O(V). In sparse graphs, W might be much smaller than V.

The solution is optimal for the BFS traversal problem as it visits each node and edge exactly once, achieving the theoretical lower bound for graph traversal algorithms. The complexity matches the fundamental requirements of BFS - we must examine all reachable nodes and their connections to ensure complete traversal.

The algorithm is near-optimal because any correct BFS implementation must process each node and edge at least once to guarantee all reachable nodes are visited, making O(V + E) the best possible time complexity for this problem.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty grid: When the input grid is empty or contains no valid cells, BFS should return immediately with a result indicating no traversal is possible
  • Single cell grid: A 1x1 grid with valid or invalid content requires careful handling of the base case
  • Single row/column: Grids with only one row or column can lead to edge case errors in neighbor calculation

Duplicate Values and Repeated States

  • Visited state tracking: Without proper visited set tracking, BFS can revisit the same cell multiple times, causing infinite loops or incorrect path counts
  • State duplication: When using complex state objects (like coordinates with additional metadata), duplicate states can be created if not properly deduplicated

Boundary Conditions

  • Grid boundaries: Accessing cells outside the grid dimensions can cause ArrayIndexOutOfBoundsException
  • Cell validation: Checking whether a cell is valid (e.g., not blocked or out of bounds) before adding to queue is critical
  • Queue overflow: In very large grids, ensure queue size doesn't grow uncontrollably (though this is typically bounded by grid dimensions)

Common Implementation Mistakes

  • Incorrect neighbor calculation: For 4-directional BFS, forgetting to check all four directions (up, down, left, right) or incorrectly calculating neighbor indices
  • Missing visited set: Not tracking visited cells leads to cycles and incorrect results in grid traversal problems
  • Queue processing order: Processing queue elements in wrong order (e.g., not processing all elements at current level before advancing to next level) can break BFS semantics
  • State initialization: Not properly initializing the starting state or distance tracking variables before BFS begins

Memory Management Pitfalls

  • Unbounded queue growth: In problems where grid size is unbounded or very large, queue can grow to consume excessive memory
  • Inefficient visited tracking: Using a 2D boolean array instead of a Set for visited cells can lead to memory inefficiency in sparse grids
// Example of proper boundary checking:
if (row >= 0 && row < grid.length && 
    col >= 0 && col < grid[0].length && 
    !visited[row][col] && 
    isValidCell(grid[row][col])) {
    // Safe to process
}

9. Alternative Approaches

There are several alternative approaches to solving problems that might seem like they could use BFS queue patterns, each with their own trade-offs:

DFS with Stack/Recursion: For problems where we need to explore all paths or track state, DFS can be an alternative. However, BFS is generally preferred when we want the shortest path or level-by-level processing. DFS might be more memory-efficient for deep trees but can lead to stack overflow issues.

Multi-source BFS: Instead of starting from a single node, we can initialize the queue with multiple sources. This is particularly useful in problems like "Rotting Oranges" or "01 Matrix" where multiple cells can be processed simultaneously. This approach is essentially a variation of standard BFS but with a different initialization.

Bidirectional BFS: When we know both the start and end states, we can perform BFS from both directions simultaneously. This can reduce time complexity from O(V+E) to approximately O((V+E)/2) in some cases, though it adds complexity in tracking the meeting point.

Dijkstra's Algorithm: For weighted graphs where edge weights are non-negative, Dijkstra's algorithm with a priority queue can be used instead of BFS. While BFS is optimal for unweighted graphs, Dijkstra's handles weighted scenarios but at higher time complexity (O((V+E)log V)).

Floyd-Warshall Algorithm: For all-pairs shortest path problems, Floyd-Warshall can be used but with O(V³) time complexity, making it less suitable for large graphs where BFS or Dijkstra would be more efficient.

The chosen BFS approach is optimal for unweighted graph traversal and shortest path problems, maintaining O(V+E) time complexity while being more intuitive than alternatives like Dijkstra's for unweighted cases.

10. Test Cases

import java.util.*;

public class TestCases {
    public static void main(String[] args) {
        // Test Case 1: Basic valid case
        int[][] grid1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
        // Expected: 14 (path 1->2->3->6->9)
        
        // Test Case 2: Single cell
        int[][] grid2 = {{5}};
        // Expected: 5
        
        // Test Case 3: Single row
        int[][] grid3 = {{1, 2, 3, 4}};
        // Expected: 10 (path 1->2->3->4)
        
        // Test Case 4: Single column
        int[][] grid4 = {{1}, {2}, {3}, {4}};
        // Expected: 10 (path 1->2->3->4)
        
        // Test Case 5: All negative values
        int[][] grid5 = {{-1, -2, -3}, {-4, -5, -6}, {-7, -8, -9}};
        // Expected: -1 (path -1->-2->-3->-6->-9)
        
        // Test Case 6: Mixed positive and negative
        int[][] grid6 = {{1, -2, 3}, {-4, 5, -6}, {7, -8, 9}};
        // Expected: 13 (path 1->2->5->9)
        
        // Test Case 7: Empty grid (edge case)
        int[][] grid7 = {};
        // Expected: 0 or handle appropriately
        
        // Test Case 8: Grid with zero values
        int[][] grid8 = {{1, 0, 3}, {4, 5, 6}, {7, 8, 9}};
        // Expected: 21 (path 1->0->3->6->9)
    }
}

Sample Test Cases:

Basic Valid Case: [[1,2,3],[4,5,6],[7,8,9]] → Expected: 14

  • Path: 1→2→3→6→9 (sum = 14)
  • Validates the algorithm handles normal 2D grid traversal

Single Cell: [[5]] → Expected: 5

  • Edge case with minimal input
  • Validates base case handling

Single Row: [[1,2,3,4]] → Expected: 10

  • Validates horizontal path handling
  • Tests 1D-like scenario in 2D context

Single Column: [[1],[2],[3],[4]] → Expected: 10

  • Validates vertical path handling
  • Tests column-wise traversal

All Negative Values: [[−1,−2,−3],[−4,−5,−6],[−7,−8,−9]] → Expected: −1

  • Validates handling of negative numbers
  • Tests that we don't greedily avoid negatives

Mixed Values: [[1,−2,3],[−4,5,−6],[7,−8,9]] → Expected: 13

  • Complex case with mixed signs
  • Validates optimal path selection

Empty Grid: {} → Expected: 0 or appropriate error handling

  • Boundary condition for robustness

Zero Values: [[1,0,3],[4,5,6],[7,8,9]] → Expected: 21

  • Validates zero handling in path calculation
  • Tests that zeros don't break the algorithm

Tricky Case:

Grid with negative path: [[1,−2,3],[4,5,−6],[7,8,9]] → Expected: 13

  • Path 1→2→5→9 = 13 (not 1→2→3→6→9 = 15)
  • Demonstrates that we're looking for maximum sum, not just greedy path

Boundary Case:

Single element: [[5]] → Expected: 5

  • Minimal case that should work without edge case handling
  • Validates single element grid processing

11. Final Summary

The core idea is to leverage BFS queue traversal to systematically explore all possible states level by level, ensuring optimal solution discovery through systematic state expansion. This approach works efficiently by maintaining a queue of unvisited states and processing them in breadth-first order, guaranteeing that the first complete solution found is optimal. The solution's efficiency stems from avoiding redundant state exploration through careful state tracking and early termination when target conditions are met.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Problems

    Flood Fill Algorithm Implementation and Analysis

    Complete Java solution for flood fill algorithm with BFS/DFS approaches, time complexity analysis, and practical implementation considerations for FAANG interviews.

  • DSA Problems

    Max Area of Island Java Solution

    Complete Java implementation for finding maximum area of island in 2D grid using DFS and BFS approaches with time and space complexity analysis.

  • DSA Problems

    Pacific Atlantic Water Flow Problem Solution

    Complete Java implementation for finding cells where water can flow to both Pacific and Atlantic oceans in a grid.

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.