🎉 All 30 days are live — the full DSA-30 course, from Big-O to System Design. See the roadmap →

Breadth-First Search (BFS)

Swap one line of DFS — a stack for a queue — and you don’t just change the order nodes are visited; you gain a superpower DFS can’t match: the first time BFS reaches any node, it has reached it by the shortest path. That single guarantee is why “fewest moves,” “minimum steps,” and “closest exit” problems all reduce to BFS.

Breadth-first search is the algorithm that explores layer by layer — visit all the neighbours of the start, then all of their unvisited neighbours, and so on. It uses a queue (the Day 4 data structure) instead of a stack.

The single most important property: BFS finds the shortest path (in number of edges) from the start to every node it reaches. That one property is the reason BFS exists.

Step through it

Press Next to watch BFS spread outward from A one layer at a time. Notice how C is visited before D and E — even though DFS visited D and E first on the exact same graph.

BFS from A — visits A, then B and C (distance 1), then D, E, F (distance 2)
ABCDEF
6 nodes · 5 edges
step 0/6

DFS dove deep into one branch first. BFS spreads out radially. Same graph, same start — different exploration order.

The template

The single line of difference from DFS: use a queue (FIFO) instead of a stack (LIFO).

Predict first
BFS marks a node 'visited' when it ENQUEUES it. What breaks if you instead mark visited only when you dequeue (pop) it?
⚠️

Mark visited when you push, not when you pop. A common bug: if you only mark visited at pop time, the same node can be added to the queue multiple times by different predecessors — you’ll process it more than once and your distance counts will be wrong. Always mark on push.

Recall the core loop — fill in the two lines that record a neighbour the moment you discover it:

python · fill in the blanks0/2 hints
from collections import deque
def bfs(start, adj):
visited = {start}
queue = deque([start])
while queue:
node = queue.popleft()
for nei in adj[node]:
if nei not in visited:
# ??? mark + enqueue on discovery (push, not pop)
# ??? mark + enqueue on discovery (push, not pop)

Why BFS finds the shortest path

Each layer of the BFS exploration corresponds to a fixed distance from the start. The start is distance 0. Its neighbours are distance 1. Their unvisited neighbours are distance 2. And so on.

The queue’s FIFO property ensures we always finish a layer before starting the next — and the first time we see any node X, we’ve reached it through the shortest possible path. (If a shorter path existed, we’d have seen X in an earlier layer.)

Computing distances

To get distances (not just visited-ness), store the distance alongside the node:

Level-by-level BFS

Some problems need you to process one entire layer at a time (e.g., “level order traversal of a tree”). The standard trick: snapshot the queue size at the top of each iteration.

def level_order(start, adj):
    visited = {start}
    queue = deque([start])
    levels = []
    while queue:
        size = len(queue)
        layer = []
        for _ in range(size):
            node = queue.popleft()
            layer.append(node)
            for nei in adj[node]:
                if nei not in visited:
                    visited.add(nei)
                    queue.append(nei)
        levels.append(layer)
    return levels

size captures the count of nodes in the current layer. The inner loop processes exactly those nodes, after which the queue holds only the next layer.

Multi-source BFS

Sometimes you want shortest distances from multiple sources simultaneously. Examples: “how far is every cell from the nearest exit?”, or Rotting Oranges where every rotten orange spreads at the same time.

Just start with all the sources in the queue and distance 0:

def multi_source_bfs(sources, adj):
    dist = {s: 0 for s in sources}
    queue = deque(sources)
    while queue:
        node = queue.popleft()
        for nei in adj[node]:
            if nei not in dist:
                dist[nei] = dist[node] + 1
                queue.append(nei)
    return dist

Same template, just multiple starting points. Each cell’s distance is to the nearest source.

What BFS is good at

  • Shortest path on unweighted graphs. The killer use case. Single source, single (or all) destinations.
  • “Levels” or “layers.” Anything you’d phrase as “after k steps.”
  • Detecting bipartiteness. Color the start red, all neighbours blue, all their neighbours red, etc. If two adjacent nodes ever get the same color, it’s not bipartite.
  • Multi-source spread. Rotting Oranges, infection spread, flood fill from multiple seeds.
  • Word ladder / state-space search where each transition has cost 1.

What BFS is bad at

  • Weighted shortest paths. BFS assumes all edges are weight 1. For varying weights, use Dijkstra (Day 21).
  • Counting paths or finding all paths. DFS is more natural — though both work.
  • Deep recursive structure (DAG topological sort, tree backtracking) — DFS reads more naturally.

Complexity

QuantityCost
TimeO(V + E)
Space (queue)O(V)
Space (visited)O(V)

Each node enters and exits the queue at most once. Each edge is examined twice in an undirected graph. Same O(V + E) as DFS — the difference is the order of visits, not the cost.

The pattern to memorize

BFS = “explore everything one step away, then everything two steps away, then three…”

Use a queue. Mark visited on push. Process layer by layer if you care about distance.

Quick check

BFS finds shortest paths on UNWEIGHTED graphs. Why does it stop working once edges have varying positive weights?

You’ve got the layer-by-layer traversal and its shortest-path guarantee. Next: Depth-First Search — the same O(V+E) traversal with a stack instead of a queue, which dives deep first and is the natural fit for cycles, components, and backtracking.

Finished this page?