BFS Queue Memory Explosion — Visited Set Timing Pitfall
- BFS uses a queue (deque) — FIFO ensures level-by-level traversal.
- Mark nodes as visited when enqueuing, not when dequeuing — prevents duplicate processing.
- BFS finds the shortest path in unweighted graphs because it processes nodes in order of distance.
- BFS explores a graph level by level: all neighbours at distance 1 before any at distance 2
- Uses a FIFO queue (deque) — O(1) enqueue and dequeue
- First time you reach a node is the shortest hop-count path in unweighted graphs
- Time: O(V+E), Space: O(V) for queue and visited set
- Mark nodes visited when enqueuing — prevents duplicate work and O(V²) queue blow-up
- Biggest mistake: using list.pop(0) as a queue — O(n) per dequeue kills performance
BFS Debugging Cheat Sheet
Queue size grows unchecked
visited.add(node) before queue.append(node)print(len(queue), len(visited)) per iterationShortest path wrong
for edge in graph[node]: check if edge is tuple with weightrun Dijkstra on same graph and compare distancesBFS not exploring all reachable nodes
print('Visited set size:', len(visited), 'Queue size:', len(queue))compare against expected node countProduction Incident
Production Debug GuideSymptom → Action: What to check when BFS isn't working as expected
How BFS Works — Plain English and Step-by-Step
BFS answers the question: 'What are all the nodes I can reach, in order of how many hops they are from my starting point?' Think of dropping a stone in a pond — ripples spread outward one ring at a time, and BFS visits nodes the same way: every node at distance 1 first, then every node at distance 2, and so on.
The algorithm requires three ingredients: a queue (FIFO) to decide the order of visiting, a visited set to avoid revisiting nodes, and the graph itself.
Step-by-step: 1. Add the start node to the queue and mark it visited. 2. While the queue is non-empty, dequeue the front node. 3. Process the dequeued node (record it, check a goal, etc.). 4. For each neighbour of the current node: if unvisited, mark it visited and enqueue it. 5. Repeat from step 2.
Worked example on the graph A-B, A-C, B-D, B-E, C-F starting from A: Round 1: dequeue A. Enqueue B, C. Visited={A,B,C}. Order=[A] Round 2: dequeue B. Enqueue D, E (A already visited). Visited={A,B,C,D,E}. Order=[A,B] Round 3: dequeue C. Enqueue F (A visited). Visited={A,B,C,D,E,F}. Order=[A,B,C] Round 4: dequeue D. No new neighbours. Order=[A,B,C,D] Round 5: dequeue E. F already visited. Order=[A,B,C,D,E] Round 6: dequeue F. Done. Order=[A,B,C,D,E,F]
Notice B and C (distance 1 from A) both appear before D, E, F (distance 2). That level-by-level guarantee is why BFS finds shortest paths in unweighted graphs: the first time you reach a node, you have used the fewest possible hops.
BFS on a Graph
The implementation uses Python's collections.deque, which provides O(1) append and popleft. Never use a plain list as a BFS queue — list.pop(0) is O(n) and will make your BFS O(V*E) instead of O(V+E). The visited set uses hashing for O(1) membership checks, which is essential when the graph is large.
# Package: io.thecodeforge.python.algorithms from collections import deque def bfs(graph, start): """Visit all reachable nodes in BFS order.""" visited = set() queue = deque([start]) visited.add(start) order = [] while queue: node = queue.popleft() # FIFO — dequeue from left order.append(node) for neighbour in graph[node]: if neighbour not in visited: visited.add(neighbour) queue.append(neighbour) # enqueue to right return order # Adjacency list representation graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } print(bfs(graph, 'A')) # ['A', 'B', 'C', 'D', 'E', 'F']
Shortest Path with BFS
BFS naturally finds the shortest path in an unweighted graph. Track the parent of each node to reconstruct the path.
# Package: io.thecodeforge.python.algorithms from collections import deque def shortest_path(graph, start, end): if start == end: return [start] visited = {start} queue = deque([[start]]) # each item is a path while queue: path = queue.popleft() node = path[-1] for neighbour in graph[node]: if neighbour not in visited: new_path = path + [neighbour] if neighbour == end: return new_path visited.add(neighbour) queue.append(new_path) return None # no path exists graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } print(shortest_path(graph, 'A', 'F')) # ['A', 'C', 'F'] print(shortest_path(graph, 'A', 'D')) # ['A', 'B', 'D']
['A', 'B', 'D']
Level-Order Tree Traversal
Level-order traversal is BFS applied to a binary tree. To separate the output by level, snapshot the queue length before processing each level. That snapshot tells you exactly how many nodes belong to the current level — process exactly that many, collecting children for the next level as you go. This avoids the need for sentinel values or separate level-tracking variables.
# Package: io.thecodeforge.python.algorithms from collections import deque class TreeNode: def __init__(self, val, left=None, right=None): self.val, self.left, self.right = val, left, right def level_order(root): if not root: return [] result = [] queue = deque([root]) while queue: level_size = len(queue) # nodes at this level level = [] for _ in range(level_size): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) return result # Build tree: 1 # / \ # 2 3 # / \ \ # 4 5 6 root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3, None, TreeNode(6))) print(level_order(root)) # [[1], [2, 3], [4, 5, 6]]
BFS Applications — Connected Components & Bipartite Check
Beyond shortest paths, BFS is the go-to for two classic problems:
Connected Components — In an undirected graph, run BFS from each unvisited node. Every BFS run discovers one connected component. Count the runs. This is used in social network analysis (friend groups), image segmentation, and network reliability.
Bipartite Check — A graph is bipartite if you can color nodes with two colors such that no edge connects same-colored nodes. BFS works perfectly: assign start node color 0, then assign neighbors color 1, then their neighbors color 0, and so on. If you ever see an edge connecting same-colored nodes, the graph is not bipartite. This is used in scheduling problems (e.g., assigning tasks to two machines without conflicts) and in recommendation systems.
# Package: io.thecodeforge.python.algorithms from collections import deque def connected_components(graph): visited = set() components = [] for node in graph: if node not in visited: component = [] queue = deque([node]) visited.add(node) while queue: n = queue.popleft() component.append(n) for nei in graph[n]: if nei not in visited: visited.add(nei) queue.append(nei) components.append(component) return components def is_bipartite(graph): color = {} for node in graph: if node not in color: queue = deque([node]) color[node] = 0 while queue: n = queue.popleft() for nei in graph[n]: if nei not in color: color[nei] = 1 - color[n] queue.append(nei) elif color[nei] == color[n]: return False return True graph = { 0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2] } print(connected_components(graph)) # [[0,1,2,3]] one component print(is_bipartite(graph)) # True graph2 = { 0: [1, 2, 3], 1: [0, 2], 2: [0, 1, 3], 3: [0, 2] } print(is_bipartite(graph2)) # False (triangle with extra edge)
True
False
BFS on a Grid — Shortest Path in 2D Matrices
BFS is ideal for grid-based shortest path problems (e.g., shortest path in a maze). The grid is a graph where each cell is a node, and edges exist to adjacent cells (up, down, left, right). Use BFS to find the minimum number of steps from start to target.
Key trick: represent each cell's neighbors by delta arrays (dx, dy). Use a visited 2D array (same size as grid) or a set of tuples. The queue stores (row, col, steps) or better yet, store (row, col) and track steps in a separate distance array.
This pattern appears in countless interview problems and real applications like robot path planning, game map navigation, and network routing in mesh grids.
# Package: io.thecodeforge.python.algorithms from collections import deque def shortest_path_grid(grid, start, target): """grid: list of lists, 0 = open, 1 = blocked.""" rows, cols = len(grid), len(grid[0]) sr, sc = start tr, tc = target if grid[sr][sc] == 1 or grid[tr][tc] == 1: return -1 visited = [[False]*cols for _ in range(rows)] queue = deque() queue.append((sr, sc)) visited[sr][sc] = True steps = 0 dirs = [(-1,0), (1,0), (0,-1), (0,1)] # up, down, left, right while queue: for _ in range(len(queue)): # process level by level to count steps r, c = queue.popleft() if (r, c) == target: return steps for dr, dc in dirs: nr, nc = r+dr, c+dc if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc] and grid[nr][nc] == 0: visited[nr][nc] = True queue.append((nr, nc)) steps += 1 return -1 # unreachable # Example grid = [ [0, 0, 0, 0], [1, 1, 0, 1], [0, 0, 0, 0], [0, 1, 1, 0] ] print(shortest_path_grid(grid, (0,0), (3,3))) # steps = 6 (correct)
| Property | BFS | DFS |
|---|---|---|
| Data structure | Queue (FIFO) | Stack (LIFO) or recursion |
| Space complexity | O(V) in worst case (queue holds level) | O(V) in worst case (call stack depth) |
| Shortest path (unweighted) | Yes — first visit guarantees shortest hop count | No — path may be long, needs full exploration |
| Cycle detection | Possible but not efficient | Natural with recursion stack |
| Topological sort | Can use Kahn's algorithm (BFS variant) | Natural with post-order DFS traversal |
| Connected components | BFS from each unvisited node | DFS from each unvisited node |
| Bipartite check | Yes — BFS with two-coloring | Yes — DFS with two-coloring |
| Memory usage dense graph | Queue can hold many nodes (level width) | Stack depth typical smaller unless deep recursion |
| Memory usage sparse graph | Similar | Similar |
| When to use | Shortest path, level-order, closer nodes first | Path existence, all paths, topological sort, maze solving with backtracking |
🎯 Key Takeaways
- BFS uses a queue (deque) — FIFO ensures level-by-level traversal.
- Mark nodes as visited when enqueuing, not when dequeuing — prevents duplicate processing.
- BFS finds the shortest path in unweighted graphs because it processes nodes in order of distance.
- Time: O(V + E); Space: O(V) for the visited set and queue.
- For level-order traversal, snapshot queue size before the inner loop to group nodes by level.
- BFS on a grid: use direction deltas and level-by-level step counting.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat data structure does BFS use and why?JuniorReveal
- QWhat is the time and space complexity of BFS?JuniorReveal
- QWhy is BFS preferred over DFS for finding shortest paths in unweighted graphs?Mid-levelReveal
- QHow would you detect if a graph is bipartite using BFS?Mid-levelReveal
- QCan BFS be used on weighted graphs for shortest path?SeniorReveal
Frequently Asked Questions
When should I use BFS vs DFS?
Use BFS for shortest path in unweighted graphs, level-order traversal, and finding closest matches. Use DFS for detecting cycles, topological sort, path existence (not shortest), and exploring all possibilities (combinatorics). BFS uses more memory (all nodes at current level) while DFS uses less (just the current path).
Why must I mark nodes as visited when enqueuing, not when dequeuing?
If you mark when dequeuing, the same node can be added to the queue multiple times before it is processed. This causes O(V²) queue size in dense graphs and incorrect shortest path computations. Mark when enqueuing to ensure each node enters the queue exactly once.
Does BFS work on disconnected graphs?
A single BFS call only visits nodes reachable from the start node. For a disconnected graph, loop over all nodes and call BFS on any that have not been visited — the same pattern used to count connected components.
What is the space complexity of BFS in the worst case?
O(V). In the worst case (a star graph), the queue holds all V-1 neighbours simultaneously after the first dequeue. The visited set also grows to O(V).
Can BFS be implemented recursively?
Technically yes, but it's unnatural — you'd need to pass the queue as a parameter. Iterative BFS is standard and more efficient because recursion adds call stack overhead.
How do I find the shortest path in a grid using BFS?
Treat each cell as a node with edges to adjacent cells. Use a queue of (row, col) and a visited boolean grid. Process level by level: increment steps after each level. When you reach the target, return steps. This gives the minimum number of moves (assuming each move costs 1).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.