Pattern 14 of 24 · Graphs · 35 min

Graph BFS and DFS

Explore grids and graphs with a visited set: DFS to flood regions, BFS for the fewest steps.

Spot it:grid of cellsislands, regions, connectedfewest steps / movesspreads each minuteJavaScript you'll use: Graphs and grids in JavaScript Stacks, queues and deques Map and Set
Say it in 30 seconds
  • O(V + E)Time for DFS or BFS on an adjacency list
  • O(V)Space: visited marks plus the queue or stack
  • E ≤ 4VEdges on a grid of V cells
Show what to say

“When I see a grid or graph and the question is islands, connected regions or the fewest steps, I traverse with a visited mark. DFS dives and floods a region; BFS spreads in rings, so it reaches every node along the fewest edges. Both are O(V + E) if I mark nodes when I push them.”

Trade-offs

  • BFS vs DFS: only BFS gives the fewest steps with equal costs; for reachability or region size both are O(V + E).
  • Mark on push vs on pop: push gives one entry per node; marking on dequeue can queue a node once per neighbour, growing to O(E).
  • Recursive vs iterative DFS: recursion mirrors the definition but JavaScript allows about ten thousand nested calls; a 300 × 300 grid of land needs 90,000.

Bring it up before they ask

Mark the start node (and every source) when you push it, check bounds before indexing the grid, and use a head index instead of queue.shift().

0 of 0 sections ticked
30-second answer

A grid is a graph in disguise: every cell is a node joined to its neighbours. DFS dives deep (flood a region). BFS spreads in rings, so it finds the fewest steps. Both are O(V + E).

When to reach for it

Spot it in the problem0 of 4

Tap the signals you can see in the problem.

RemembergridIslands, regions, connectedfewest stepsspreads

Pick your variant
  • DFS

    Nodes wait inthe call stack, or an array used as one

    flood fillnumber of islandsmax area of island
  • BFS

    Nodes wait inan array queue with a head index

    shortest path in a gridword ladder
  • Multi-source BFS

    Nodes wait inone queue seeded with every source

    rotting orangesdistance to the nearest exit
  • Search back from the targets

    Nodes wait ineither

    Pacific Atlantic water flowsurrounded regions

The core idea: visit every node once

Exploring a maze, you keep a to-do list of places seen but not yet visited.

Read more · 79 words

Take one, add its unseen neighbours. The kind of list decides how you explore:

  • A stack (newest first) dives down one corridor: depth-first.
  • A queue (oldest first) spreads like ripples in a pond, finishing every node at distance d before d + 1: breadth-first.

Grids are full of loops, so you need a visited mark. Mark a node when you push it, so nothing is queued twice. To count pieces, scan every node: each unmarked one starts a new piece.

A queue is a line at a ticket window: people wait in order and are served one at a time.

DFS with a stack: flood each island once

grid (4×5)
01234
011001
110011
200100
301100
stack (0)
empty
islands
0

Scan the grid row by row. Water is greyed out; every "1" is land that no search has reached yet.

function numIslands(grid) {
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  let islands = 0;
  for (let r = 0; r < grid.length; r++) {
    for (let c = 0; c < grid[0].length; c++) {
      if (grid[r][c] !== "1") continue;
      islands++;
      grid[r][c] = "#";              // mark on push
      const stack = [[r, c]];
      while (stack.length > 0) {
        const [cr, cc] = stack.pop();
        for (const [dr, dc] of dirs) {
          const nr = cr + dr, nc = cc + dc;
          if (grid[nr]?.[nc] !== "1") continue;
          grid[nr][nc] = "#";
          stack.push([nr, nc]);
        }
      }
    }
  }
  return islands;
}
All steps as text
  1. Scan the grid row by row. Water is greyed out; every "1" is land that no search has reached yet.
  2. (0,0) is land that no earlier flood reached, so it starts island 1. Mark it and push it onto the stack.
  3. Pop (0,0) from the top of the stack and look at its four neighbours. Unmarked land next to (0,0): (1,0), (0,1).
  4. Mark (1,0) now, as it is pushed, not later when it is popped. Then no other cell can push it a second time.
  5. Mark (0,1) and push it.
  6. Pop (0,1), the newest cell. (1,0) waits underneath: last in, first out, so DFS finishes the newest branch before going back. (0,1) has no unmarked land around it, so this branch ends here.
  7. Pop (1,0). (1,0) has no unmarked land around it, so this branch ends here.
  8. The stack is empty: every cell of island 1 is marked. Back to the scan.
  9. The scan reaches (0,1). It was land, but the flood already marked it "#", so it is skipped: that is why no island is counted twice.
  10. (0,4) is land that no earlier flood reached, so it starts island 2. Mark it and push it onto the stack.
  11. Pop (0,4). Unmarked land next to (0,4): (1,4).
  12. Mark (1,4) and push it.
  13. Pop (1,4). Unmarked land next to (1,4): (1,3).
  14. Mark (1,3) and push it.
  15. Pop (1,3). (1,3) has no unmarked land around it, so this branch ends here.
  16. The stack is empty: every cell of island 2 is marked. Back to the scan.
  17. (2,2) is land that no earlier flood reached, so it starts island 3. Mark it and push it onto the stack.
  18. Pop (2,2). Unmarked land next to (2,2): (3,2).
  19. Mark (3,2) and push it.
  20. Pop (3,2). Unmarked land next to (3,2): (3,1).
  21. Mark (3,1) and push it.
  22. Pop (3,1). (3,1) has no unmarked land around it, so this branch ends here.
  23. The stack is empty: every cell of island 3 is marked. Back to the scan.
  24. The scan is finished. It started 3 floods, one per island.

Play with it

Paint walls, move the start and goal, then race a queue against a stack.

Read more · 90 words

BFS always finds the shortest path; DFS often does not.

Step by step

  1. Name the nodes and the edges

    A grid node is (r, c).

  2. Pick DFS or BFS

    Fewest steps: BFS. Reachable, how many, how big: either.

  3. Choose the visited mark

    A seen grid or Set, or a dist array with -1 for unvisited.

  4. Seed the search

    One start, every unmarked node, or all sources at once.

  5. Expand neighbours behind guards

    Skip out-of-bounds, blocked or marked cells.

  6. Read the answer off the traversal

    Searches started, flood size, or distance.

Interactive

Maze flood: BFS vs DFS

Paint walls, move the start and goal, then race a queue against a stack. BFS spreads in rings; DFS dives down one corridor.

Visited: 0 of 59 open cellsNeighbour checks: 0Path: searching

Paint walls, then press Run. BFS uses a queue, so it finishes every cell at distance d before any at d + 1.

Speed and maze

Building the graph and DFS

Building the graph

Graphs arrive as an edge list ([[0, 1], [1, 2]]), an adjacency list (graph[i] lists the neighbours of i), or an implicit graph (a grid).

Read more · 31 words

Convert an edge list once, so finding neighbours is fast:

For a grid, build nothing. Use a directions array, [[1, 0], [-1, 0], [0, 1], [0, -1]], behind a bounds check.

// Turn an edge list into an adjacency list once, up front: adj[u] is the array of u's neighbours, so
// looping over a node's neighbours costs O(its degree) instead of a scan over every edge.
export function buildAdjacency(n, edges, directed = false) {
  const adj = Array.from({ length: n }, () => []); // n separate arrays (not fill([]), which shares one)
  for (const [a, b] of edges) {
    adj[a].push(b);
    if (!directed) adj[b].push(a);                 // an undirected edge is stored in both directions
  }
  return adj;
}

// When nodes are strings or sparse numbers ("JFK", 10^9), key a Map by node instead of using an array.
export function buildAdjacencyMap(edges, directed = false) {
  const adj = new Map();
  const add = (from, to) => {
    if (!adj.has(from)) adj.set(from, []);
    if (to !== undefined) adj.get(from).push(to);
  };
  for (const [a, b] of edges) {
    add(a, b);
    if (directed) add(b);                          // make sure sinks exist as keys too
    else add(b, a);
  }
  return adj;
}

Template 1: DFS for regions

Use DFS when the question is what can be reached, not how far.

Read more · 46 words

The recursive version reads like the definition ("this cell, plus everything reachable from its neighbours") and can return values up, as Max Area of IslandMedium does to add up a size. The iterative version can't overflow the call stack on a huge region.

This solves Flood FillEasy, Number of IslandsMedium and Max Area of IslandMedium.

// Template 1a: recursive DFS on a grid. A grid is an implicit graph: cell (r, c) is a node and its
// neighbours are the in-bounds cells one step away. Counts the regions of connected cells equal to `target`.
export function countRegions(grid, target = 1) {
  const rows = grid.length;
  const cols = rows > 0 ? grid[0].length : 0;
  const seen = Array.from({ length: rows }, () => new Array(cols).fill(false));
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

  function dfs(r, c) {
    seen[r][c] = true;                                          // mark before exploring further
    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; // off the grid
      if (seen[nr][nc] || grid[nr][nc] !== target) continue;      // visited, or not part of a region
      dfs(nr, nc);
    }
  }

  let regions = 0;
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === target && !seen[r][c]) {
        regions++;                                              // nothing earlier reached it: a new region
        dfs(r, c);
      }
    }
  }
  return regions;
}

// Template 1b: the same flood with an explicit stack, so a huge region can't overflow the call stack.
// Returns the size of every region in the order the scan finds them.
export function regionSizes(grid, target = 1) {
  const rows = grid.length;
  const cols = rows > 0 ? grid[0].length : 0;
  const seen = Array.from({ length: rows }, () => new Array(cols).fill(false));
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  const sizes = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] !== target || seen[r][c]) continue;
      seen[r][c] = true;                                        // mark on push
      const stack = [[r, c]];
      let size = 0;
      while (stack.length > 0) {
        const [cr, cc] = stack.pop();
        size++;
        for (const [dr, dc] of dirs) {
          const nr = cr + dr;
          const nc = cc + dc;
          if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
          if (seen[nr][nc] || grid[nr][nc] !== target) continue;
          seen[nr][nc] = true;
          stack.push([nr, nc]);
        }
      }
      sizes.push(size);
    }
  }
  return sizes;
}

BFS and multi-source BFS

Template 2: BFS for the fewest steps

A node is first found along a shortest path.

Read more · 41 words

Set dist[next] = dist[node] + 1 then, and every distance comes out in one pass.

Use a head index, not queue.shift(), which can make BFS O(V²).

For "how many steps", process one level at a time, as Word LadderHard does.

BFS: fewest edges from a start node

graph
0123456
queue (0)
empty
adj (size 7)
0→1, 21→0, 32→0, 3, 43→1, 2, 54→2, 5, 65→3, 46→4
head
0

Turn the edge list into an adjacency list first. Each undirected edge is stored in both directions, so a node's neighbours are one lookup away.

function bfs(n, edges, start) {
  const adj = Array.from({ length: n }, () => []);
  for (const [a, b] of edges) { adj[a].push(b); adj[b].push(a); }
  const dist = new Array(n).fill(-1);    // -1 = not visited
  dist[start] = 0;
  const queue = [start];
  for (let head = 0; head < queue.length; head++) {
    const node = queue[head];
    for (const next of adj[node]) {
      if (dist[next] !== -1) continue;
      dist[next] = dist[node] + 1;
      queue.push(next);
    }
  }
  return dist;
}
All steps as text
  1. Turn the edge list into an adjacency list first. Each undirected edge is stored in both directions, so a node's neighbours are one lookup away.
  2. Start at 0 with distance 0. It is marked (given a distance) the moment it enters the queue, like every node after it.
  3. Dequeue 0 and look at its neighbours. The queue only ever holds distances d and d + 1, so nodes leave it in order of distance.
  4. 1 has no distance yet, so this is the first, and therefore shortest, way to reach it: 0 + 1 = 1. Mark it and enqueue it.
  5. 2 is new: distance 0 + 1 = 1. Mark it and enqueue it.
  6. Dequeue 1. Every node at distance 0 is finished, so the nodes at distance 1 start coming out.
  7. 0 already has its distance (0), which can't be improved now: skip it.
  8. 3 is new: distance 1 + 1 = 2. Mark it and enqueue it.
  9. Dequeue 2 (distance 1) and look at its neighbours.
  10. 0 already has its distance (0), which can't be improved now: skip it.
  11. 3 is already waiting in the queue with distance 2. Because it was marked when enqueued, it isn't added a second time.
  12. 4 is new: distance 1 + 1 = 2. Mark it and enqueue it.
  13. Dequeue 3. Every node at distance 1 is finished, so the nodes at distance 2 start coming out.
  14. 1 already has its distance (1), which can't be improved now: skip it.
  15. 2 already has its distance (1), which can't be improved now: skip it.
  16. 5 is new: distance 2 + 1 = 3. Mark it and enqueue it.
  17. Dequeue 4 (distance 2) and look at its neighbours.
  18. 2 already has its distance (1), which can't be improved now: skip it.
  19. 5 is already waiting in the queue with distance 3. Because it was marked when enqueued, it isn't added a second time.
  20. 6 is new: distance 2 + 1 = 3. Mark it and enqueue it.
  21. Dequeue 5. Every node at distance 2 is finished, so the nodes at distance 3 start coming out.
  22. 3 already has its distance (2), which can't be improved now: skip it.
  23. 4 already has its distance (2), which can't be improved now: skip it.
  24. Dequeue 6 (distance 3) and look at its neighbours.
  25. 4 already has its distance (2), which can't be improved now: skip it.
  26. The queue is empty. Every reachable node has its fewest-edges distance from 0. The highlighted edges form a shortest-path tree.
// Template 2: BFS from one start node. Nodes come out of the queue in order of distance, so the first time a
// node is discovered is along a fewest-edges path. dist[v] = number of edges from start, or -1 if unreachable.
export function bfsDistances(adj, start) {
  const dist = new Array(adj.length).fill(-1);  // -1 doubles as "not visited"
  dist[start] = 0;                              // mark the start when it is enqueued
  const queue = [start];
  for (let head = 0; head < queue.length; head++) { // head index: queue.shift() would be O(n) per call
    const node = queue[head];
    for (const next of adj[node]) {
      if (dist[next] !== -1) continue;          // already queued, so its distance is final
      dist[next] = dist[node] + 1;              // mark on enqueue
      queue.push(next);
    }
  }
  return dist;
}

// Fewest moves between two cells of a grid of strings ("." open, "#" wall), one level per step. -1 if blocked.
export function gridSteps(grid, [sr, sc], [tr, tc]) {
  const rows = grid.length;
  const cols = grid[0].length;
  if (grid[sr][sc] === "#" || grid[tr][tc] === "#") return -1;
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  const seen = Array.from({ length: rows }, () => new Array(cols).fill(false));
  seen[sr][sc] = true;
  const queue = [[sr, sc]];
  let head = 0;
  for (let steps = 0; head < queue.length; steps++) {
    const levelEnd = queue.length;              // everything queued so far is `steps` moves away
    while (head < levelEnd) {
      const [r, c] = queue[head++];
      if (r === tr && c === tc) return steps;
      for (const [dr, dc] of dirs) {
        const nr = r + dr;
        const nc = c + dc;
        if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
        if (seen[nr][nc] || grid[nr][nc] === "#") continue;
        seen[nr][nc] = true;
        queue.push([nr, nc]);
      }
    }
  }
  return -1;
}

Template 3: multi-source BFS

How far is each cell from the nearest source (rotten orange, exit)?

Read more · 49 words

Don't run one BFS per source. Queue all sources at distance 0 and run once, as if one imaginary source touched them all.

Count the levels to get the time until everything is reached: Rotting OrangesMedium. It also runs backwards: in Pacific Atlantic Water FlowMedium, start from the ocean edges and climb uphill.

Multi-source BFS: distance to the nearest exit

dist (4×5)
01234
0···#·
1·#···
2·#·#·
3·····
queue (0)
empty
head
0

Exits are "E", walls are "#". Every open cell starts unreached (·). We want each cell's distance to its nearest exit.

function nearestExit(grid) {
  const rows = grid.length, cols = grid[0].length;
  const dist = Array.from({ length: rows }, () => new Array(cols).fill(-1));
  const queue = [];
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      if (grid[r][c] === "E") { dist[r][c] = 0; queue.push([r, c]); }
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  for (let head = 0; head < queue.length; head++) {
    const [r, c] = queue[head];
    for (const [dr, dc] of dirs) {
      const nr = r + dr, nc = c + dc;
      if (grid[nr]?.[nc] !== "." || dist[nr][nc] !== -1) continue;
      dist[nr][nc] = dist[r][c] + 1;
      queue.push([nr, nc]);
    }
  }
  return dist;
}
All steps as text
  1. Exits are "E", walls are "#". Every open cell starts unreached (·). We want each cell's distance to its nearest exit.
  2. Put every exit, (0,0) and (2,4), in the queue at distance 0 before searching. They spread at the same time, as if one imaginary source were joined to all of them.
  3. Pop (0,0) at distance 0. Its unreached neighbours (1,0) and (0,1) get distance 1. This is the first time any exit reaches them, so it is their distance to the nearest exit.
  4. Pop (2,4) at distance 0. Its unreached neighbours (3,4) and (1,4) get distance 1.
  5. Pop (1,0) at distance 1. Its unreached neighbour (2,0) gets distance 2.
  6. Pop (0,1) at distance 1. Its unreached neighbour (0,2) gets distance 2.
  7. Pop (3,4) at distance 1. Its unreached neighbour (3,3) gets distance 2.
  8. Pop (1,4) at distance 1. Its unreached neighbours (0,4) and (1,3) get distance 2.
  9. Pop (2,0) at distance 2. Its unreached neighbour (3,0) gets distance 3.
  10. Pop (0,2) at distance 2. Its unreached neighbour (1,2) gets distance 3.
  11. Pop (3,3) at distance 2. Its unreached neighbour (3,2) gets distance 3.
  12. Pop (0,4) at distance 2. Its neighbours are walls, the edge, or already have a distance from a nearer or equally near exit: nothing to add.
  13. Pop (1,3) at distance 2. Its neighbours are walls, the edge, or already have a distance from a nearer or equally near exit: nothing to add.
  14. Pop (3,0) at distance 3. Its unreached neighbour (3,1) gets distance 4.
  15. Pop (1,2) at distance 3. Its unreached neighbour (2,2) gets distance 4.
  16. Pop (3,2) at distance 3. Its neighbours are walls, the edge, or already have a distance from a nearer or equally near exit: nothing to add.
  17. Pop (3,1) at distance 4. Its neighbours are walls, the edge, or already have a distance from a nearer or equally near exit: nothing to add.
  18. Pop (2,2) at distance 4. Its neighbours are walls, the edge, or already have a distance from a nearer or equally near exit: nothing to add.
  19. The queue is empty after one pass over the grid, however many exits there are. Every open cell was reached.
// Template 3: multi-source BFS. Every source starts in the queue at distance 0, so one pass gives each cell its
// distance to the NEAREST source. Grid of strings: "E" exit (a source), "." open, "#" wall; -1 = unreachable.
export function distanceToNearest(grid) {
  const rows = grid.length;
  const cols = grid[0].length;
  const dist = Array.from({ length: rows }, () => new Array(cols).fill(-1));
  const queue = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === "E") {
        dist[r][c] = 0;                           // all sources are seeded (and marked) together
        queue.push([r, c]);
      }
    }
  }
  const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
  for (let head = 0; head < queue.length; head++) {
    const [r, c] = queue[head];
    for (const [dr, dc] of dirs) {
      const nr = r + dr;
      const nc = c + dc;
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
      if (grid[nr][nc] !== "." || dist[nr][nc] !== -1) continue;
      dist[nr][nc] = dist[r][c] + 1;              // first discovery = nearest source
      queue.push([nr, nc]);
    }
  }
  return dist;
}

Try it

try-it.js
// Edit the inputs and press Run. The functions are the templates above.
const adj = buildAdjacency(6, [[0, 1], [0, 2], [1, 3], [2, 3], [4, 5]]);
console.log(adj);                       // [[1, 2], [0, 3], [0, 3], [1, 2], [5], [4]]
console.log(bfsDistances(adj, 0));      // [0, 1, 1, 2, -1, -1]: 4 and 5 are unreachable

const land = [
  [1, 1, 0, 0, 1],
  [1, 0, 0, 1, 1],
  [0, 0, 1, 0, 1],
];
console.log(countRegions(land), regionSizes(land)); // 3 [3, 4, 1]

const maze = ['..#.', '.##.', '....'];
console.log(gridSteps(maze, [0, 0], [0, 3]));       // 7

console.log(distanceToNearest(['E..', '.#.', '..E'])); // [[0, 1, 2], [1, -1, 1], [2, 1, 0]]

Complexity and JavaScript

Complexity

Read more · 120 words
TraversalTimeExtra space
DFS or BFS on an adjacency listO(V + E)O(V)
DFS or BFS on an m × n gridO(m·n): four neighbour checks per cellO(m·n)
Counting components (scan + flood)O(V + E)O(V)
Multi-source BFS, any number of sourcesO(V + E)O(V)
Building an adjacency listO(V + E)O(V + E)

JavaScript you'll use

JavaScript refresher: Graphs and grids in JavaScript Stacks, queues and deques Map and Set

new Array(rows).fill(new Array(cols).fill(false)) puts the same row in every slot. And seen.has([r, c]) is always false: use a string or number key.

const adj = Array.from({ length: n }, () => []);                        // n separate arrays
const seen = Array.from({ length: rows }, () => new Array(cols).fill(false));
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; /* … */ }
if (grid[nr]?.[nc] !== "1") continue;      // out-of-bounds reads give undefined instead of throwing
for (let head = 0; head < queue.length; head++) { const node = queue[head]; /* … */ }
const key = `${r},${c}`;                   // a cell as a Set or Map key (or r * cols + c)

Interview questions

DFS or BFS: when does the choice matter?

For fewest steps with equal costs, only BFS is correct (with weights, see Shortest Paths). For reachability, both work.

Why mark a node visited when it is enqueued, not dequeued?

Otherwise a node can be queued once per neighbour, and the queue grows to O(E). Marking on enqueue gives one entry per node.

You can't modify the input grid. What changes?

Keep a separate mark: a boolean grid or a Uint8Array(rows * cols).

How would you return the shortest path itself?

Store parent[next] = node, then follow parents back from the target and reverse.

The graph is implicit and enormous. How can you make BFS faster?

Bidirectional BFS: search from both ends until they meet, about 2·b^(d/2) nodes instead of b^d.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. You need the fewest moves from the top-left to the bottom-right of a maze where every move costs the same. Which search guarantees the right answer?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: When edges mean 'must come before', order the nodes topologically.