Steven's Knowledge

BFS & DFS

Breadth-first and depth-first traversal for graphs, trees, and grids -- when to use which, with iterative and recursive implementations

BFS & DFS

Breadth-first search (BFS) and depth-first search (DFS) are the two fundamental ways to explore a graph or tree. Almost every graph problem reduces to one of these two traversals with some bookkeeping on top.

BFS vs DFS at a Glance

BFSDFS
Data structureQueueStack (or recursion)
ExploresLevel by levelAs deep as possible first
Best forShortest path (unweighted), level-orderExhaustive search, cycle detection, topological sort
SpaceO(width of tree/graph)O(depth of tree/graph)
Finds shortest path?Yes (unweighted)Not guaranteed

Rule of thumb: if the problem mentions "shortest," "minimum steps," or "closest," use BFS. If it mentions "all paths," "connected components," or "can you reach," DFS is often simpler.

BFS Template

function bfs(start: string, graph: Map<string, string[]>): void {
  const queue: string[] = [start];
  const visited = new Set<string>([start]);

  while (queue.length > 0) {
    const node = queue.shift()!;
    // Process node

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
}

BFS with Level Tracking

Many problems need to know which "level" you are on (distance from start, tree depth, etc.).

function bfsWithLevels(start: string, graph: Map<string, string[]>): number {
  const queue: string[] = [start];
  const visited = new Set<string>([start]);
  let level = 0;

  while (queue.length > 0) {
    const levelSize = queue.length; // snapshot current level size

    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()!;
      // Process node at this level

      for (const neighbor of graph.get(node) ?? []) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }

    level++;
  }

  return level;
}

The key trick: snapshot queue.length before processing. All nodes dequeued in that batch are at the same level.

DFS Template (Recursive)

function dfs(
  node: string,
  graph: Map<string, string[]>,
  visited: Set<string>
): void {
  visited.add(node);
  // Process node

  for (const neighbor of graph.get(node) ?? []) {
    if (!visited.has(neighbor)) {
      dfs(neighbor, graph, visited);
    }
  }
}

DFS Template (Iterative with Stack)

Iterative DFS avoids stack overflow on deep graphs and gives you more control.

function dfsIterative(start: string, graph: Map<string, string[]>): void {
  const stack: string[] = [start];
  const visited = new Set<string>();

  while (stack.length > 0) {
    const node = stack.pop()!;

    if (visited.has(node)) continue;
    visited.add(node);
    // Process node

    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        stack.push(neighbor);
      }
    }
  }
}

Note: iterative DFS processes nodes in a different order than recursive DFS (neighbors are visited in reverse). This usually does not matter, but be aware of it.

Grid Traversal

Grids are graphs where each cell connects to its 4 (or 8) neighbors. BFS/DFS on grids is extremely common.

Number of Islands

Count connected components of '1's in a 2D grid.

function numIslands(grid: string[][]): number {
  if (grid.length === 0) return 0;

  const rows = grid.length;
  const cols = grid[0].length;
  let count = 0;

  function dfs(r: number, c: number): void {
    if (r < 0 || r >= rows || c < 0 || c >= cols) return;
    if (grid[r][c] !== "1") return;

    grid[r][c] = "0"; // mark visited by modifying grid

    dfs(r + 1, c);
    dfs(r - 1, c);
    dfs(r, c + 1);
    dfs(r, c - 1);
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === "1") {
        count++;
        dfs(r, c);
      }
    }
  }

  return count;
}

Time: O(rows * cols), Space: O(rows * cols) worst case for the recursion stack.

Grid BFS Template

function gridBfs(
  grid: number[][],
  startRow: number,
  startCol: number
): number {
  const rows = grid.length;
  const cols = grid[0].length;
  const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];

  const queue: [number, number][] = [[startRow, startCol]];
  const visited = new Set<string>();
  visited.add(`${startRow},${startCol}`);
  let steps = 0;

  while (queue.length > 0) {
    const levelSize = queue.length;

    for (let i = 0; i < levelSize; i++) {
      const [r, c] = queue.shift()!;

      // Check if we reached the goal
      // if (isGoal(r, c)) return steps;

      for (const [dr, dc] of directions) {
        const nr = r + dr;
        const nc = c + dc;
        const key = `${nr},${nc}`;

        if (
          nr >= 0 && nr < rows &&
          nc >= 0 && nc < cols &&
          !visited.has(key) &&
          grid[nr][nc] !== 0 // or whatever the wall condition is
        ) {
          visited.add(key);
          queue.push([nr, nc]);
        }
      }
    }

    steps++;
  }

  return -1; // goal not reachable
}

Word Ladder

Transform one word to another, changing one letter at a time, where each intermediate word must be in a dictionary. Find the shortest transformation sequence.

function ladderLength(
  beginWord: string,
  endWord: string,
  wordList: string[]
): number {
  const wordSet = new Set(wordList);
  if (!wordSet.has(endWord)) return 0;

  const queue: string[] = [beginWord];
  const visited = new Set<string>([beginWord]);
  let steps = 1;

  while (queue.length > 0) {
    const levelSize = queue.length;

    for (let i = 0; i < levelSize; i++) {
      const word = queue.shift()!;

      if (word === endWord) return steps;

      // Try changing each character
      for (let j = 0; j < word.length; j++) {
        for (let c = 97; c <= 122; c++) { // 'a' to 'z'
          const newWord =
            word.slice(0, j) + String.fromCharCode(c) + word.slice(j + 1);

          if (wordSet.has(newWord) && !visited.has(newWord)) {
            visited.add(newWord);
            queue.push(newWord);
          }
        }
      }
    }

    steps++;
  }

  return 0;
}

Time: O(M^2 * N) where M = word length, N = word list size. Space: O(M * N).

Clone Graph

Deep copy a graph given a reference to one node.

interface GraphNode {
  val: number;
  neighbors: GraphNode[];
}

function cloneGraph(node: GraphNode | null): GraphNode | null {
  if (!node) return null;

  const cloned = new Map<GraphNode, GraphNode>();

  function dfs(original: GraphNode): GraphNode {
    if (cloned.has(original)) return cloned.get(original)!;

    const copy: GraphNode = { val: original.val, neighbors: [] };
    cloned.set(original, copy);

    for (const neighbor of original.neighbors) {
      copy.neighbors.push(dfs(neighbor));
    }

    return copy;
  }

  return dfs(node);
}

Time: O(V + E), Space: O(V).

Tree Traversal Refresher

Trees are graphs without cycles. No need for a visited set.

interface TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
}

// DFS variants
function preorder(node: TreeNode | null): number[] {
  if (!node) return [];
  return [node.val, ...preorder(node.left), ...preorder(node.right)];
}

function inorder(node: TreeNode | null): number[] {
  if (!node) return [];
  return [...inorder(node.left), node.val, ...inorder(node.right)];
}

function postorder(node: TreeNode | null): number[] {
  if (!node) return [];
  return [...postorder(node.left), ...postorder(node.right), node.val];
}

// BFS (level-order)
function levelOrder(root: TreeNode | null): number[][] {
  if (!root) return [];
  const result: number[][] = [];
  const queue: TreeNode[] = [root];

  while (queue.length > 0) {
    const level: number[] = [];
    const size = queue.length;

    for (let i = 0; i < size; i++) {
      const node = queue.shift()!;
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(level);
  }

  return result;
}

When to Use BFS vs DFS

ScenarioUse
Shortest path in unweighted graphBFS
Level-order traversalBFS
Checking if path existsEither (DFS is simpler)
Connected componentsDFS
Topological sortDFS (with post-order)
Cycle detectionDFS
Exploring all possibilitiesDFS
Grid shortest pathBFS

Complexity Summary

ProblemTimeSpace
BFS/DFS on graphO(V + E)O(V)
Number of islandsO(R * C)O(R * C)
Word ladderO(M^2 * N)O(M * N)
Clone graphO(V + E)O(V)
Tree traversalO(n)O(h) for DFS, O(w) for BFS

BFS and DFS are the building blocks. Once you are comfortable with these templates, problems like topological sort, shortest path, and connected components become variations with added bookkeeping.

On this page