Algorithms

Depth-First Search

Dive deep, then backtrack — the recursion-friendly graph traversal

Depth-first search (DFS) explores a graph by going as deep as possible along one branch before backtracking. Driven by a stack (or recursion), it runs in O(V + E) and naturally supports cycle detection, topological sort, connected components, and any "visit everything" task. The natural opposite of breadth-first search.

  • TimeO(V + E)
  • SpaceO(depth) for the recursion stack
  • Data structure usedStack (LIFO) or recursion
  • Shortest pathNot guaranteed (use BFS or Dijkstra)
  • Natural forCycle detection, topological sort, connected components

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

How DFS works

The recursive form is the natural one:

  1. Start at the source node. Mark it visited.
  2. For each unvisited neighbor, recursively DFS from it.
  3. Return when all neighbors are processed.

That's it. The recursion stack tracks the current path. When you hit a dead end (all neighbors visited), the call returns and you backtrack to the previous node, automatically. The simplicity is why DFS is the default graph traversal in interview settings — the iterative-with-explicit-stack version produces the same output but takes 3× the code.

The iterative form swaps recursion for an explicit stack:

iterativeDFS(graph, start):
    stack = [start]
    visited = set()
    while stack not empty:
        node = stack.pop()
        if node in visited: continue
        visited.add(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                stack.push(neighbor)

Note: iterative DFS visits nodes in a slightly different order than recursive — pushing all neighbors before processing any reorders the traversal compared to recursing into them one at a time. The algorithm is still correct, just the visit order changes.

Three-color DFS for cycle detection

To detect cycles in a directed graph, mark each node with one of three states:

  • White — not yet visited.
  • Gray — currently on the recursion path (entered but not yet finished).
  • Black — finished (entered, processed, returned).

If DFS encounters a gray neighbor, that's a back edge — there's a cycle from this node back to the gray ancestor. White and black transitions don't indicate cycles; only gray-on-gray does.

function hasCycle(graph) {
  const WHITE = 0, GRAY = 1, BLACK = 2;
  const color = new Map();
  for (const node of Object.keys(graph)) color.set(node, WHITE);

  function dfs(node) {
    color.set(node, GRAY);
    for (const neighbor of graph[node] || []) {
      if (color.get(neighbor) === GRAY) return true;        // back edge → cycle
      if (color.get(neighbor) === WHITE && dfs(neighbor)) return true;
    }
    color.set(node, BLACK);
    return false;
  }

  for (const node of color.keys()) {
    if (color.get(node) === WHITE && dfs(node)) return true;
  }
  return false;
}

For undirected graphs, the rule is simpler: a back edge to any visited node (other than the immediate parent) indicates a cycle.

DFS vs BFS

DFSBFS
StrategyDive deep, backtrackExpand level by level
Data structureStack (or recursion)Queue
Memory on deep narrow graphsHigh (depth ~ V)Low (level ~ 1)
Memory on wide shallow graphsLow (depth ~ 1)High (level ~ V)
Shortest path (unweighted)No guaranteeYes — first reach is shortest
Cycle detection (directed)Natural — three colorsPossible but awkward
Topological sortNatural — finish orderKahn's algorithm
Strongly connected componentsTarjan's, Kosaraju'sNot directly

When to use DFS

  • Cycle detection. Three-color DFS is the standard algorithm.
  • Topological sort. DFS in finish-time order, reverse for the topological ordering.
  • Connected components. Run DFS from each unvisited node; each DFS call discovers exactly one component.
  • Strongly connected components. Tarjan's and Kosaraju's algorithms are both DFS-based.
  • Maze solving and backtracking. Dive into one path, hit a wall, back up, try the next. Recursive DFS makes the backtracking automatic.
  • Tree algorithms. Pre/in/post-order traversal, lowest common ancestor, tree diameter, subtree-sum problems — all DFS.
  • Memory-constrained graph search. When the graph is wide (high branching factor) but you suspect the target is near, DFS uses O(depth) memory while BFS uses O(width). For width ≫ depth, DFS wins.

JavaScript implementation

// Recursive — clearest expression of the algorithm
function dfs(graph, start, visited = new Set()) {
  if (visited.has(start)) return;
  visited.add(start);
  // process(start) — record / check goal / etc.
  for (const neighbor of graph[start] || []) {
    dfs(graph, neighbor, visited);
  }
  return visited;
}

// Iterative — works on graphs deep enough to overflow the call stack
function dfsIter(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 neighbor of graph[node] || []) {
      if (!visited.has(neighbor)) stack.push(neighbor);
    }
  }
  return visited;
}

// Topological sort using DFS finish order
function topoSort(graph) {
  const visited = new Set();
  const order = [];
  function visit(node) {
    if (visited.has(node)) return;
    visited.add(node);
    for (const neighbor of graph[node] || []) visit(neighbor);
    order.push(node); // push when finished — appears AFTER all descendants
  }
  for (const node of Object.keys(graph)) visit(node);
  return order.reverse(); // reverse finish order = topological order
}

Python implementation

def dfs(graph, start, visited=None):
    if visited is None: visited = set()
    if start in visited: return visited
    visited.add(start)
    for neighbor in graph.get(start, []):
        dfs(graph, neighbor, visited)
    return visited

# Iterative version — important when graph is too deep for recursion
def dfs_iter(graph, start):
    visited = set()
    stack = [start]
    while stack:
        node = stack.pop()
        if node in visited: continue
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                stack.append(neighbor)
    return visited

import sys
sys.setrecursionlimit(100000)  # raise if needed for deep recursion

Famous DFS problems

Number of Islands

Given a 2D grid of 1s (land) and 0s (water), count the connected components of land. Run DFS from each unvisited land cell, marking everything reachable; each DFS call increments the count by 1.

function numIslands(grid) {
  const rows = grid.length, cols = grid[0].length;
  let count = 0;
  function sink(r, c) {
    if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] !== '1') return;
    grid[r][c] = '0'; // mark visited by mutating
    sink(r-1, c); sink(r+1, c); sink(r, c-1); sink(r, c+1);
  }
  for (let r = 0; r < rows; r++)
    for (let c = 0; c < cols; c++)
      if (grid[r][c] === '1') { count++; sink(r, c); }
  return count;
}

Common bugs and edge cases

  • Stack overflow on deep graphs. Recursive DFS on 100k+ chain blows the call stack in most languages. Convert to iterative or raise the recursion limit.
  • Marking visited too early or too late. Mark on entry to the recursive call, before exploring neighbors — otherwise duplicate work or infinite loops in cyclic graphs.
  • Confusing forward edges with back edges in directed cycle detection. A forward edge to a visited (black) node is fine. A back edge to a node currently on the recursion path (gray) indicates a cycle. The three-color technique distinguishes them.
  • Visiting the parent in undirected cycle detection. In undirected graphs, every edge appears twice (once from each side). When DFS sees a "visited" neighbor that's just the immediate parent, that's not a cycle — it's the same edge you arrived on. Pass the parent down and skip it.
  • Using DFS to find shortest paths. DFS finds A path, not THE shortest. For shortest paths, use BFS (unweighted) or Dijkstra (weighted positive) or Bellman-Ford (any weights).

Frequently asked questions

How does DFS detect a cycle in a directed graph?

Use the three-colors technique. White = unvisited, gray = in the current recursion path, black = fully processed. If DFS encounters a gray node, that's a back edge — there's a cycle. White-to-gray transitions happen on entry; gray-to-black on exit. The cycle is the path from the gray node back to itself.

How is DFS used for topological sort?

For a directed acyclic graph (DAG), do DFS and push each node onto a stack as you finish processing it (after exploring all neighbors). Reverse the stack to get a topological order. The intuition: a node finishes only after all its descendants finish, so it appears earlier in the reverse output. Linear time O(V + E), beats Kahn's algorithm on density-balanced cases.

When does DFS recursion overflow the stack?

When the depth exceeds the OS limit — typically ~10,000-100,000 frames depending on language and platform. A degenerate input (linear chain of a million nodes) blows the call stack. Either convert to iterative DFS with an explicit stack on the heap (which can hold millions), or impose an iteration-depth cutoff.

What are the three classes of edges in a DFS tree?

Tree edges (the ones DFS actually traversed), back edges (point to an ancestor — indicate a cycle in directed graphs), and forward/cross edges (point to a descendant or unrelated node — only in directed graphs). The classification determines what DFS can prove about the graph: presence of back edges proves cycles, no back edges proves a DAG.

How is iterative DFS different from recursive DFS?

Same algorithm, different stack. Recursive DFS uses the call stack; iterative DFS uses an explicit array as a stack. Iterative is necessary when the recursion would overflow but produces nearly identical traversal order (subtle differences in edge ordering depending on push order). Both are O(V + E) time.

Why is DFS better than BFS for connected components?

Both work, both are O(V + E). DFS uses less memory on graphs that are deep but narrow (recursion stack tracks one branch; BFS queue tracks an entire level). On a million-node tree-shaped graph, BFS at the deepest level might queue half a million nodes; DFS only ever has the current branch in memory.

Can DFS find shortest paths?

Not in general — DFS finds A path, not THE shortest. Iterative deepening DFS (IDDFS) is a hybrid: run DFS with increasing depth limits until you find the target. It gives shortest-path correctness with DFS's memory efficiency (O(depth) instead of BFS's O(width)). Used in IDA* for AI search on huge state spaces.