Skip to content
DSA Patterns

Learn

Breadth First Search (BFS)

BFS explained: the queue template, level-by-level processing, multi-source BFS, and why BFS is the shortest-path algorithm on unweighted graphs.

3 min readUpdated 2 Sept 2026

#Graphs#Trees#Queue#Shortest Path

BFS earns its place for one reason: on an unweighted graph it finds the shortest path, and DFS does not. Any question containing the words

fewest, minimum steps, nearest or level over an unweighted structure is a BFS

question, and reaching for DFS there is a wrong answer that often still passes the first sample case.

The idea

Expand outward in rings. Visit everything one step away, then everything two steps away, and so on. A queue enforces that order automatically, because nodes come out in the order they went in.

from collections import deque

def bfs(start, neighbours):
    queue = deque([start])
    seen = {start}
    steps = 0

    while queue:
        for _ in range(len(queue)):    # snapshot: exactly one level
            node = queue.popleft()
            if is_goal(node):
                return steps
            for nxt in neighbours(node):
                if nxt not in seen:
                    seen.add(nxt)      # mark on ENQUEUE, not on dequeue
                    queue.append(nxt)
        steps += 1

    return -1

Two details carry the whole pattern.

`for _ in range(len(queue))`. Taking the queue's length before the loop snapshots

the current level, so everything enqueued inside the loop belongs to the next one. This is what makes "how many steps" and "give me the nodes level by level" both fall out of the same skeleton.

Mark seen on enqueue. Marking on dequeue lets the same node be queued several times

before it is first processed — still correct, but the queue blows up and on large inputs it times out.

Level order traversal

The tree version is the same loop with the level collected instead of counted:

def level_order(root):
    if not root:
        return []
    out, queue = [], deque([root])
    while queue:
        level = []
        for _ in range(len(queue)):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        out.append(level)
    return out

Trees need no seen set — there are no cycles and each node has one parent. Everything else in the family is a variation on what you take from level: the last value gives

Right Side View, alternating the direction gives Zigzag, the max gives

Largest Value in Each Row.

Multi-source BFS

The variant people miss. When several starting points spread simultaneously, seed the queue with all of them at once rather than running one BFS per source. The rings then expand in parallel and every cell is reached by whichever source is nearest — one O(V + E) pass instead of k of them.

def oranges_rotting(grid):
    rows, cols = len(grid), len(grid[0])
    queue = deque()
    fresh = 0

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c))     # every rotten orange is a source
            elif grid[r][c] == 1:
                fresh += 1

    minutes = 0
    while queue and fresh:
        for _ in range(len(queue)):
            r, c = queue.popleft()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                    grid[nr][nc] = 2     # the grid IS the visited set
                    fresh -= 1
                    queue.append((nr, nc))
        minutes += 1

    return -1 if fresh else minutes

01 Matrix is the same shape: seed the queue with every zero, and the ring number each

cell is reached on is its distance to the nearest zero. Trying to solve it from each 1 instead is the O(n²) trap.

Word Ladder

The one that does not look like a graph. Words are nodes, and two words are neighbours when they differ by one letter — so the shortest transformation sequence is exactly BFS. Do not build the edges by comparing every pair (that is O(n² × L)); generate neighbours on demand by replacing each position with each letter and testing membership in the word set. Bidirectional BFS — expanding from both ends and stopping when the frontiers meet — is the optimisation to mention.

Complexity

O(V + E): every node is enqueued once and every edge is examined once. Space is O(V) for the queue and the visited set — on a wide graph the queue can hold an entire level, which is BFS's real cost against DFS's O(depth) stack.

Mistakes that cost the round

  • Marking visited on dequeue. Correct but quadratic-ish in practice; it is the usual

cause of a TLE on an otherwise right answer.

  • Not snapshotting the level length, then wondering why the step count is wrong.
  • Using DFS for a shortest path on an unweighted graph. DFS finds a path.
  • Running BFS once per source when a multi-source seed would do it in one pass.
  • Using BFS on a weighted graph. Once edges have different costs, it is

Dijkstra, not BFS — unless all weights are equal.

What to drill

  1. Binary Tree Level Order Traversal — the snapshot loop.
  2. Rotting Oranges — multi-source, on a grid.
  3. 01 Matrix — multi-source distance transform.
  4. Word Ladder — implicit graph, shortest path.

All on the 22 DSA Patterns sheet.

Frequently asked

When should I use BFS instead of DFS?

Use BFS when the question asks for the fewest steps, the shortest path, the nearest thing, or anything level by level on an unweighted graph — BFS reaches every node by a shortest route, DFS does not. Use DFS for connectivity, exhaustive exploration, cycle detection, or when the recursion mirrors the structure, and when a wide graph would make BFS's queue too large.

What is multi-source BFS?

Seeding the queue with every starting point before the loop begins, so all sources expand simultaneously. Each node is then reached by whichever source is closest, in a single O(V + E) pass instead of one BFS per source. Rotting Oranges and 01 Matrix are the standard examples.

Should I mark nodes visited when enqueuing or when dequeuing?

When enqueuing. Marking on dequeue lets the same node sit in the queue many times before it is first processed — still correct, but the queue grows far larger than it needs to, and it is a common cause of time-limit failures on big inputs.

Related