Learn/DSA/Graph Traversal (BFS & DFS)
AlgorithmsIntermediate10 min

Graph Traversal (BFS & DFS)

Explore a graph without looping forever — BFS for shortest hops, DFS for reachability, and a visited set to keep both honest.

GraphsBFSDFSShortest Path

Traversing a graph is like traversing a tree — with one dangerous difference: graphs have cycles. Follow edges naively and you can loop forever, or revisit the same node again and again. The fix is a single idea that appears in every graph algorithm: a visited set that records where you have already been.

Once you guard against revisiting, the same two strategies from trees carry over. Breadth-first search (BFS) spreads outward in rings using a queue, so the first time it reaches a node is along the fewest edges — that makes it the shortest-path algorithm on an unweighted graph. Depth-first search (DFS) plunges down one path using a stack (or recursion), which is perfect for reachability and connectivity questions.

Graph Traversal
ABCDEF
order:
BFS uses a queue — explores level by level (shortest hops).
BFS expands in rings from the source (a queue); DFS dives down one branch (a stack). The visited set stops both from looping.

BFS — shortest hops in an unweighted graph

BFS processes nodes in order of distance from the source. Mark a node visited when you enqueue it (not when you dequeue it) so it never lands in the queue twice.

BFS shortest distance on an adjacency list
function bfsDistances(graph, start) {
  const dist = new Map([[start, 0]])
  const queue = [start]
  while (queue.length) {
    const node = queue.shift()
    for (const next of graph[node]) {
      if (!dist.has(next)) {          // first time seen = shortest
        dist.set(next, dist.get(node) + 1)
        queue.push(next)
      }
    }
  }
  return dist                          // node -> hops from start
}

Why BFS gives shortest paths

BFS drains the queue in distance order: all nodes at distance d are processed before any at d + 1. So the first time you reach a node is guaranteed to be along the fewest edges — provided every edge has the same weight. For weighted edges you need Dijkstra instead.

DFS — recursion or an explicit stack

DFS commits to one neighbour and follows it as deep as possible before backtracking. The recursive form is the shortest; the iterative form swaps the call stack for an explicit one, which avoids stack-overflow on huge graphs.

DFS — recursive and iterative
function dfsRecursive(graph, node, visited = new Set()) {
  visited.add(node)
  for (const next of graph[node]) {
    if (!visited.has(next)) dfsRecursive(graph, next, visited)
  }
  return visited
}

function dfsIterative(graph, start) {
  const visited = new Set()
  const stack = [start]
  while (stack.length) {
    const node = stack.pop()
    if (visited.has(node)) continue
    visited.add(node)
    for (const next of graph[node]) {
      if (!visited.has(next)) stack.push(next)
    }
  }
  return visited
}

Grids are graphs in disguise

A 2D grid is just an implicit graph: each cell is a node, and its neighbours are the (up to) four cells around it. Instead of an adjacency list you compute neighbours on the fly with direction offsets. This is the backbone of flood-fill, island-counting, and maze problems.

BFS over a grid with direction offsets
function bfsGrid(grid, sr, sc) {
  const rows = grid.length, cols = grid[0].length
  const seen = new Set([sr + ',' + sc])
  const queue = [[sr, sc]]
  const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]
  while (queue.length) {
    const [r, c] = queue.shift()
    for (const [dr, dc] of dirs) {
      const nr = r + dr, nc = c + dc
      const key = nr + ',' + nc
      if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue
      if (grid[nr][nc] === 0 || seen.has(key)) continue   // 0 = wall
      seen.add(key)
      queue.push([nr, nc])
    }
  }
  return seen
}

Connected components

A disconnected graph splits into islands of mutually reachable nodes. Loop over every node; whenever you find one that has not been visited, launch a fresh BFS/DFS from it — each launch discovers exactly one component. Counting the launches counts the components.

  • Use BFS when you need the shortest path in hops, level-by-level order, or the minimum number of steps.
  • Use DFS for reachability, cycle detection, topological sort, and exploring all paths.
  • Either one finds connected components — pick by whichever guard (queue vs stack/recursion) fits the problem.
  • Always mark visited the moment a node enters the frontier, or a cycle will trap you in an infinite loop.
OperationTime

V = vertices, E = edges. Both traversals touch every vertex and every edge once.

The one bug everyone hits

Forgetting the visited check turns any cyclic graph into an infinite loop — and on a tree-shaped input the bug hides, because trees have no cycles. Add the visited set first, even while prototyping.

Section navigation