DSA Tutorial
🔍

Queue in BFS

Why BFS Uses a Queue

Breadth-first search explores a graph level by level — all nodes at distance 1 from the source before distance 2, all at distance 2 before distance 3, and so on. The FIFO property of a queue is what enforces this order.

BFS on a graph from source S:

       S
      / \
     A   B       Distance 1 — processed AFTER S, BEFORE C,D,E,F
    / \   \
   C   D   E     Distance 2 — processed AFTER A,B, BEFORE G
  /
 G               Distance 3

Queue state during BFS:
  Start:    [S]
  Process S: enqueue A, B → [A, B]
  Process A: enqueue C, D → [B, C, D]
  Process B: enqueue E    → [C, D, E]
  Process C: enqueue G    → [D, E, G]
  Process D: (no children) → [E, G]
  Process E: (no children) → [G]
  Process G: (no children) → []

FIFO ensures A and B are both processed before C, D, E —
level-by-level exploration. A stack would dive deep immediately.

BFS Template

Every BFS problem follows the same structural template. Variations only change what is stored in the queue and what computation happens at each node.

BFS Template:

  visited = set()
  queue   = deque([start])
  visited.add(start)
  dist[start] = 0

  while queue:
      node = queue.popleft()         ← DEQUEUE (process node)

      for neighbour in graph[node]:
          if neighbour not in visited:
              visited.add(neighbour)  ← MARK ON ENQUEUE (not dequeue)
              dist[neighbour] = dist[node] + 1
              queue.append(neighbour) ← ENQUEUE

Critical rule: mark visited WHEN ENQUEUING, not when dequeuing.
  Marking on dequeue allows the same node to be enqueued multiple
  times from different neighbours, causing O(E) redundant processing
  and potentially wrong distance values.

Application 1: Level-Order Tree Traversal

Problem: Print all nodes of a binary tree level by level.

Key technique: Before processing each level, record levelSize = queue.size(). Dequeue exactly levelSize nodes — all belong to the current level. Their children form the next level.

1import java.util.*; 2 3public class LevelOrderTraversal { 4 5 static class TreeNode { 6 int val; TreeNode left, right; 7 TreeNode(int val) { this.val = val; } 8 } 9 10 // Returns list of levels — each inner list is one level 11 public static List<List<Integer>> levelOrder(TreeNode root) { 12 List<List<Integer>> result = new ArrayList<>(); 13 if (root == null) return result; 14 15 Queue<TreeNode> queue = new ArrayDeque<>(); 16 queue.offer(root); 17 18 while (!queue.isEmpty()) { 19 int levelSize = queue.size(); // Number of nodes at this level 20 List<Integer> level = new ArrayList<>(); 21 22 for (int i = 0; i < levelSize; i++) { 23 TreeNode node = queue.poll(); 24 level.add(node.val); 25 26 if (node.left != null) queue.offer(node.left); 27 if (node.right != null) queue.offer(node.right); 28 } 29 30 result.add(level); 31 } 32 33 return result; 34 } 35 36 // Variant: right side view — last element of each level 37 public static List<Integer> rightSideView(TreeNode root) { 38 List<Integer> result = new ArrayList<>(); 39 if (root == null) return result; 40 41 Queue<TreeNode> queue = new ArrayDeque<>(); 42 queue.offer(root); 43 44 while (!queue.isEmpty()) { 45 int levelSize = queue.size(); 46 for (int i = 0; i < levelSize; i++) { 47 TreeNode node = queue.poll(); 48 if (i == levelSize - 1) result.add(node.val); // Last = rightmost 49 50 if (node.left != null) queue.offer(node.left); 51 if (node.right != null) queue.offer(node.right); 52 } 53 } 54 return result; 55 } 56 57 public static void main(String[] args) { 58 // 3 59 // / \ 60 // 9 20 61 // / \ 62 // 15 7 63 TreeNode root = new TreeNode(3); 64 root.left = new TreeNode(9); 65 root.right = new TreeNode(20); 66 root.right.left = new TreeNode(15); 67 root.right.right = new TreeNode(7); 68 69 System.out.println(levelOrder(root)); // [[3], [9, 20], [15, 7]] 70 System.out.println(rightSideView(root)); // [3, 20, 7] 71 } 72}
Output:
[[3], [9, 20], [15, 7]]
[3, 20, 7]

Dry Run: Level-Order on the Tree Above

Tree:       3
           / \
          9  20
            /  \
           15   7

queue=[3], result=[]

Level 1: levelSize=1
  dequeue 3 → level=[3] → enqueue 9, 20
  result=[[3]], queue=[9,20]

Level 2: levelSize=2
  dequeue 9  → level=[9]  → no children
  dequeue 20 → level=[9,20] → enqueue 15, 7
  result=[[3],[9,20]], queue=[15,7]

Level 3: levelSize=2
  dequeue 15 → level=[15] → no children
  dequeue 7  → level=[15,7] → no children
  result=[[3],[9,20],[15,7]], queue=[]

The key: process exactly levelSize nodes per iteration.
Children enqueued during one level-loop belong to the NEXT level.

Application 2: Shortest Path in Unweighted Graph

Problem: Find the shortest path (minimum number of edges) from source to target.

1import java.util.*; 2 3public class ShortestPathBFS { 4 5 public static int shortestPath(Map<Integer, List<Integer>> graph, 6 int source, int target) { 7 if (source == target) return 0; 8 9 Queue<Integer> queue = new ArrayDeque<>(); 10 Map<Integer, Integer> dist = new HashMap<>(); 11 12 queue.offer(source); 13 dist.put(source, 0); 14 15 while (!queue.isEmpty()) { 16 int node = queue.poll(); 17 18 for (int neighbour : graph.getOrDefault(node, Collections.emptyList())) { 19 if (!dist.containsKey(neighbour)) { 20 dist.put(neighbour, dist.get(node) + 1); 21 22 if (neighbour == target) return dist.get(neighbour); 23 24 queue.offer(neighbour); 25 } 26 } 27 } 28 29 return -1; // Target not reachable 30 } 31 32 // Also return the actual path 33 public static List<Integer> shortestPathNodes( 34 Map<Integer, List<Integer>> graph, int source, int target) { 35 36 Queue<Integer> queue = new ArrayDeque<>(); 37 Map<Integer,Integer> parent = new HashMap<>(); 38 39 queue.offer(source); 40 parent.put(source, -1); // Source has no parent 41 42 while (!queue.isEmpty()) { 43 int node = queue.poll(); 44 if (node == target) break; 45 46 for (int nb : graph.getOrDefault(node, Collections.emptyList())) { 47 if (!parent.containsKey(nb)) { 48 parent.put(nb, node); 49 queue.offer(nb); 50 } 51 } 52 } 53 54 if (!parent.containsKey(target)) return Collections.emptyList(); 55 56 // Reconstruct path from target back to source 57 List<Integer> path = new ArrayList<>(); 58 for (int curr = target; curr != -1; curr = parent.get(curr)) { 59 path.add(0, curr); 60 } 61 return path; 62 } 63 64 public static void main(String[] args) { 65 Map<Integer, List<Integer>> g = new HashMap<>(); 66 g.put(0, Arrays.asList(1, 2)); 67 g.put(1, Arrays.asList(0, 3, 4)); 68 g.put(2, Arrays.asList(0, 4)); 69 g.put(3, Arrays.asList(1, 5)); 70 g.put(4, Arrays.asList(1, 2, 5)); 71 g.put(5, Arrays.asList(3, 4)); 72 73 System.out.println(shortestPath(g, 0, 5)); // 3 74 System.out.println(shortestPathNodes(g, 0, 5)); // [0, 1, 4, 5] or similar 75 } 76}
Output:
3
[0, 1, 4, 5]

Application 3: Rotting Oranges — Multi-Source BFS

Problem: A grid has fresh oranges (1), rotten oranges (2), and empty cells (0). Every minute, each rotten orange infects adjacent fresh ones. Return the minimum minutes until no fresh oranges remain, or -1 if impossible.

Multi-source BFS: All rotten oranges start simultaneously at minute 0. BFS fans out from all sources at once — each fresh orange is reached at the time of the nearest rotten orange.

1import java.util.*; 2 3public class RottingOranges { 4 5 public static int orangesRotting(int[][] grid) { 6 int rows = grid.length, cols = grid[0].length; 7 Queue<int[]> queue = new ArrayDeque<>(); 8 int fresh = 0; 9 10 // Step 1: Find all initially rotten oranges (sources) and count fresh 11 for (int r = 0; r < rows; r++) { 12 for (int c = 0; c < cols; c++) { 13 if (grid[r][c] == 2) queue.offer(new int[]{r, c, 0}); // {row, col, time} 14 else if (grid[r][c] == 1) fresh++; 15 } 16 } 17 18 if (fresh == 0) return 0; // No fresh oranges 19 20 int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; 21 int maxTime = 0; 22 23 // Step 2: Multi-source BFS 24 while (!queue.isEmpty()) { 25 int[] curr = queue.poll(); 26 int r = curr[0], c = curr[1], time = curr[2]; 27 28 for (int[] d : dirs) { 29 int nr = r + d[0], nc = c + d[1]; 30 31 if (nr >= 0 && nr < rows && nc >= 0 && nc < cols 32 && grid[nr][nc] == 1) { 33 grid[nr][nc] = 2; // Mark rotten 34 fresh--; 35 maxTime = Math.max(maxTime, time + 1); 36 queue.offer(new int[]{nr, nc, time + 1}); 37 } 38 } 39 } 40 41 return fresh == 0 ? maxTime : -1; // -1 if fresh oranges remain 42 } 43 44 public static void main(String[] args) { 45 int[][] g1 = {{2,1,1},{1,1,0},{0,1,1}}; 46 int[][] g2 = {{2,1,1},{0,1,1},{1,0,1}}; 47 int[][] g3 = {{0,2}}; 48 49 System.out.println(orangesRotting(g1)); // 4 50 System.out.println(orangesRotting(g2)); // -1 (bottom-left isolated) 51 System.out.println(orangesRotting(g3)); // 0 (no fresh oranges) 52 } 53}
Output:
4
-1
0

Dry Run: [[2,1,1],[1,1,0],[0,1,1]]

Grid:           Initial rotten: (0,0)   Fresh: 6
  2 1 1
  1 1 0
  0 1 1

queue=[(0,0,t=0)], fresh=6

Process (0,0,0): infect (0,1)→t=1, (1,0)→t=1
  grid: 2 2 1        queue=[(0,1,1),(1,0,1)], fresh=4
        2 1 0
        0 1 1

Process (0,1,1): infect (0,2)→t=2, (1,1)→t=2
  grid: 2 2 2        queue=[(1,0,1),(0,2,2),(1,1,2)], fresh=2
        2 2 0
        0 1 1

Process (1,0,1): no fresh neighbours (all adjacent are 0 or already rotten)

Process (0,2,2): no fresh neighbours

Process (1,1,2): infect (2,1)→t=3
  grid: 2 2 2        queue=[(2,1,3)], fresh=1
        2 2 0
        0 2 1

Process (2,1,3): infect (2,2)→t=4
  grid: 2 2 2        queue=[(2,2,4)], fresh=0
        2 2 0
        0 2 2

Process (2,2,4): no fresh neighbours

fresh=0, maxTime=4 → return 4 ✓

Application 4: 01 Matrix — Distance to Nearest 0

Problem: Given a binary matrix, find the distance to the nearest 0 for every cell.

Multi-source BFS approach: Treat ALL 0-cells as sources at distance 0. BFS simultaneously from all of them — each 1-cell is reached at the distance of its nearest 0.

1import java.util.*; 2 3public class ZeroOneMatrix { 4 5 public static int[][] updateMatrix(int[][] mat) { 6 int rows = mat.length, cols = mat[0].length; 7 int[][] dist = new int[rows][cols]; 8 Queue<int[]> q = new ArrayDeque<>(); 9 10 // Seed all 0-cells at distance 0; mark 1-cells as unvisited (MAX_VALUE) 11 for (int r = 0; r < rows; r++) { 12 for (int c = 0; c < cols; c++) { 13 if (mat[r][c] == 0) { 14 dist[r][c] = 0; 15 q.offer(new int[]{r, c}); 16 } else { 17 dist[r][c] = Integer.MAX_VALUE; 18 } 19 } 20 } 21 22 int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; 23 24 // Multi-source BFS 25 while (!q.isEmpty()) { 26 int[] curr = q.poll(); 27 int r = curr[0], c = curr[1]; 28 29 for (int[] d : dirs) { 30 int nr = r + d[0], nc = c + d[1]; 31 if (nr >= 0 && nr < rows && nc >= 0 && nc < cols 32 && dist[nr][nc] > dist[r][c] + 1) { 33 dist[nr][nc] = dist[r][c] + 1; 34 q.offer(new int[]{nr, nc}); 35 } 36 } 37 } 38 39 return dist; 40 } 41 42 public static void main(String[] args) { 43 int[][] m1 = {{0,0,0},{0,1,0},{0,0,0}}; 44 int[][] m2 = {{0,0,0},{0,1,0},{1,1,1}}; 45 46 int[][] r1 = updateMatrix(m1); 47 for (int[] row : r1) System.out.println(Arrays.toString(row)); 48 // [0,0,0] [0,1,0] [0,0,0] 49 50 System.out.println("---"); 51 int[][] r2 = updateMatrix(m2); 52 for (int[] row : r2) System.out.println(Arrays.toString(row)); 53 // [0,0,0] [0,1,0] [1,2,1] 54 } 55}
Output:
[0, 0, 0]
[0, 1, 0]
[1, 2, 1]

Application 5: Word Ladder

Problem: Given a beginWord, endWord, and a word list, find the length of the shortest transformation sequence from beginWord to endWord where each step changes exactly one character and every intermediate word must be in the word list.

BFS insight: Each word is a node; two words are connected if they differ by exactly one character. BFS finds the shortest path — fewest word transformations.

1import java.util.*; 2 3public class WordLadder { 4 5 public static int ladderLength(String beginWord, String endWord, List<String> wordList) { 6 Set<String> wordSet = new HashSet<>(wordList); 7 if (!wordSet.contains(endWord)) return 0; 8 9 Queue<String> queue = new ArrayDeque<>(); 10 Set<String> visited = new HashSet<>(); 11 12 queue.offer(beginWord); 13 visited.add(beginWord); 14 int steps = 1; 15 16 while (!queue.isEmpty()) { 17 int levelSize = queue.size(); 18 steps++; 19 20 for (int i = 0; i < levelSize; i++) { 21 String word = queue.poll(); 22 char[] chars = word.toCharArray(); 23 24 // Try changing each character to every letter a-z 25 for (int j = 0; j < chars.length; j++) { 26 char original = chars[j]; 27 28 for (char c = 'a'; c <= 'z'; c++) { 29 if (c == original) continue; 30 chars[j] = c; 31 String next = new String(chars); 32 33 if (next.equals(endWord)) return steps; 34 35 if (wordSet.contains(next) && !visited.contains(next)) { 36 visited.add(next); 37 queue.offer(next); 38 } 39 } 40 41 chars[j] = original; // Restore 42 } 43 } 44 } 45 46 return 0; // No transformation found 47 } 48 49 public static void main(String[] args) { 50 System.out.println(ladderLength("hit", "cog", 51 Arrays.asList("hot","dot","dog","lot","log","cog"))); // 5 52 System.out.println(ladderLength("hit", "cog", 53 Arrays.asList("hot","dot","dog","lot","log"))); // 0 54 } 55}
Output:
5
0

Dry Run: Word Ladder hit → cog

wordSet = {hot, dot, dog, lot, log, cog}
queue = [(hit, steps=1)], visited = {hit}

Level 1 (steps=1 → steps becomes 2):
  Word: hit
  Try h→a..z: no match; Try i→o: hot ∈ wordSet, not visited
    → visited.add(hot), queue=[hot]

Level 2 (steps=2 → steps becomes 3):
  Word: hot
  Try h→d: dot ∈ wordSet → queue=[dot]
  Try h→l: lot ∈ wordSet → queue=[dot,lot]

Level 3 (steps=3 → steps becomes 4):
  Word: dot
  Try d→l: lot already visited
  Try o→o... t→g: dog ∈ wordSet → queue=[lot,dog]
  Word: lot
  Try l→d: dot already visited
  Try t→g: log ∈ wordSet → queue=[lot,dog,log]

Level 4 (steps=4 → steps becomes 5):
  Word: dog
  Try d→c: cog == endWord! → return steps=5 ✓

Path: hit → hot → dot → dog → cog (5 words = 5 steps)

BFS Complexity Summary

ProblemTimeSpaceBFS Variant
Level-order traversalO(n)O(w) max level widthSingle-source, tree
Shortest path (graph)O(V + E)O(V)Single-source, graph
Rotting orangesO(rows × cols)O(rows × cols)Multi-source grid BFS
01 MatrixO(rows × cols)O(rows × cols)Multi-source grid BFS
Word LadderO(N × M × 26)O(N × M)Single-source word graph

N = word list size, M = word length, 26 = alphabet size.

Common Mistakes

Marking visited on dequeue instead of enqueue. The same node can be enqueued multiple times from different neighbours before it is ever dequeued. When finally dequeued, it processes each neighbour again — O(E) redundant work and potentially wrong distance values. Always mark visited (or update distance) at the moment of enqueueing.

Not using levelSize for level boundaries. Processing children directly inside the loop without recording levelSize = queue.size() at the start mixes levels — children are processed at the same step as their parents. Record the count before the inner loop; dequeue exactly that many nodes.

Forgetting to seed all sources for multi-source BFS. Multi-source BFS requires all source nodes to be in the queue at the start, all at distance 0. Seeding only one source and running BFS, then another, is both incorrect and O(S × (V+E)) instead of O(V+E).

Not checking grid bounds before accessing grid[nr][nc]. Accessing grid[-1][0] or grid[rows][0] is undefined behavior in C++ and an IndexError in Python/Java. Always check nr >= 0 && nr < rows && nc >= 0 && nc < cols before reading or writing the grid.

Using Array.shift() inside BFS in JavaScript. shift() is O(n) — in a BFS over n nodes this gives O(n²). Use an index pointer qi advancing through the array, or use a proper O(1) queue implementation for large inputs.

Interview Questions

Q: Why does BFS guarantee the shortest path in an unweighted graph but not in a weighted one?

In an unweighted graph, all edges have equal cost. BFS processes nodes in non-decreasing distance order — first all nodes at distance 1, then distance 2, and so on. The first time a node is reached, it is via the fewest edges. In a weighted graph, a path with more edges can be shorter in total weight — BFS does not account for edge weights and may miss the shortest path. Dijkstra's algorithm uses a priority queue to always process the globally cheapest unvisited node, correctly handling varying edge weights.

Q: What is multi-source BFS and when is it more efficient than running BFS from each source?

Multi-source BFS initialises the queue with all source nodes at distance 0 simultaneously and runs a single BFS pass. This computes the shortest distance from the nearest source to every node in O(V + E). Running individual BFS from each of k sources takes O(k × (V + E)). For problems like "distance to nearest 0" or "time until all oranges rot," multi-source BFS is optimal. It is equivalent to adding a virtual super-source connected to all real sources with zero-weight edges.

Q: How do you track the actual shortest path (not just its length) in BFS?

Maintain a parent map alongside the distance map. When a node neighbour is first reached from node, set parent[neighbour] = node. After the BFS completes, reconstruct the path by following parent pointers backward from the target to the source, then reversing. The parent map uses O(V) additional space.

FAQs

Can BFS detect cycles in a graph?

Yes. When a neighbour of the current node is already in the visited set (and is not the parent in an undirected graph), a cycle exists. For directed graphs, any back edge to an already-visited node indicates a cycle. BFS is less commonly used for cycle detection than DFS — DFS naturally reveals back edges during its recursive structure — but BFS can detect cycles correctly.

Is BFS always better than DFS for shortest paths?

BFS finds the shortest path (fewest edges) in unweighted graphs and is preferred for that purpose. DFS is better for: detecting cycles, topological sort, finding all connected components, and problems requiring complete path exploration. For weighted shortest paths, neither BFS nor DFS is appropriate — use Dijkstra (non-negative weights) or Bellman-Ford (negative weights).

What is bidirectional BFS and when does it help?

Bidirectional BFS runs two simultaneous BFS explorations — one from the source, one from the target. They meet somewhere in the middle. If the shortest path has length d, standard BFS explores O(b^d) nodes (b = branching factor). Bidirectional BFS explores O(2 × b^(d/2)) nodes — exponentially fewer for large d. It is used in the Word Ladder problem and social network shortest paths where the graph is large but well-connected.

Summary

BFS uses a queue's FIFO property to explore a graph layer by layer — guaranteed to find the shortest path (fewest edges) in unweighted graphs.

The canonical BFS template:

  • Initialise queue with source(s) at distance 0
  • Mark visited on enqueue (not dequeue) to prevent redundant processing
  • For each dequeued node, process it and enqueue all unvisited neighbours
  • Level boundaries tracked by recording levelSize = queue.size() before each inner loop

Five core applications:

  • Level-order traversal — record levelSize before inner loop; process that many nodes exactly; all their children form the next level
  • Shortest path — standard single-source BFS; parent map reconstructs the actual path
  • Rotting oranges / 01 matrix — multi-source BFS; seed all sources at distance 0; BFS fans out simultaneously
  • Word Ladder — word graph BFS; generate neighbours by substituting each character position with a-z
  • Dijkstra — priority queue replaces plain queue; processes by minimum distance, not by level

Complexity: O(V + E) time, O(V) space for graph BFS. For grid BFS: O(rows × cols) time and space.

Suggested Quiz

Why does BFS use a queue instead of a stack?

1/6