BeeTool.Dev - Essential Tools for developers

Stories.Tool Tabs VaultPersonal Documents
Super Debugging Context
BeeTool.Dev

Essential tools, knowledge, and playgrounds for developers.

Explore

  • Stories
  • Personal Documents
  • Features
  • Algorithms Playground

Tool Tabs

  • JSON Compare
  • SQL Formatter
  • Text Diff
  • All tools →

Learn

  • Sorting algorithms
  • Searching algorithms
  • Algorithm Race Mode
© 2024 – 2026 BeeTool.DevPrivacy policy
← Algorithms Playground
Searching · Intermediate

Depth-First Search (DFS)

Commit to one path until it dead-ends, then backtrack — the traversal that turns recursion itself into a graph explorer.

AverageO(V + E)StructureGraphNeeds sortedNoComparisonYes
1074196155889363343579
Must remember

Go deep first, backtrack at dead ends — a stack (usually the call stack) plus a visited set.

Time O(V + E) · Space O(V) worst (recursion/stack depth)

Hands on Interactive visualization

Target
86
Drag nodes to rearrange
6154894713169685316867887
Frontier
empty
visited 0 · depth 0
0/0Ready — press play
Speed
Step 0 / 0
Comparisons
0
Swaps
0
Reads
0
Writes
0
Elapsed
0.0s
Frames
0
Depth
0 / 0
Aux memory
0 B

Implementation Code

export function dfs(
  adj: Map<number, number[]>,
  node: number,
  target: number,
  visited = new Set<number>(),
): boolean {
  if (visited.has(node)) return false; // breadcrumb already here
  visited.add(node); // pre-order: act before descending
  if (node === target) return true;
  for (const nb of adj.get(node) ?? []) {
    if (dfs(adj, nb, target, visited)) return true;
  }
  return false; // dead end: unwind to the last junction
}

// Iterative variant for deep graphs: explicit stack, no recursion limit.
export function dfsIter(adj: Map<number, number[]>, start: number, target: number) {
  const visited = new Set<number>();
  const stack = [start];
  while (stack.length > 0) {
    const node = stack.pop()!; // LIFO makes it depth-first
    if (visited.has(node)) continue;
    visited.add(node);
    if (node === target) return true;
    stack.push(...(adj.get(node) ?? []));
  }
  return false;
}

Big-O Complexity

Best
O(1)
Average
O(V + E)
Worst
O(V + E)
Space
O(V)

Space is the visited set plus the recursion (or explicit) stack; the stack grows with path depth, which is O(V) in the worst case — a single long chain. On wide, shallow graphs DFS's stack is far smaller than BFS's frontier.

In production When to use

Topological sort

Build systems, package managers, and task schedulers order dependencies by DFS post-order reversed — a node is finished only after everything it depends on, so finish times ARE the topological order.

Cycle detection

Deadlock detection, circular-import checks, and DAG validation find cycles as back edges: an edge into a node still on the current recursion stack (the gray node in white/gray/black coloring).

Connected components

Flood one DFS from each unvisited node to label components — image segmentation, network partition analysis, and union-of-islands problems. Strongly connected components (Tarjan, Kosaraju) are DFS-based too.

Maze & puzzle solving

A maze is a graph of cells; DFS walks one corridor to its end, backtracks at dead ends, and is the natural engine when any path suffices (and randomized DFS generates the maze in the first place).

Backtracking search

N-queens, Sudoku, subset/permutation generation, and regex engines are DFS over an implicit tree of partial solutions — go deep, fail, unwind one choice, try the next branch.

Filesystem & AST walks

Directory tree traversal, JSON/AST visitors, and reference-following serializers are pre- or post-order DFS — the recursion mirrors the nesting of the structure itself.

Trade-offs Strengths & weaknesses

  • O(V) space bounded by path depth — much lighter than BFS on wide graphs.
  • The recursive form is a few lines: the call stack does the bookkeeping for free.
  • Exposes deep structure BFS can't: discovery/finish times, back edges, topological order, SCCs.
  • The natural engine for backtracking — state unwinds automatically as calls return.
  • Can strike deep targets quickly instead of exhaustively filling every closer level first.
  • Finds a path, not the shortest path — no distance guarantee whatsoever.
  • Recursive form overflows the call stack on deep graphs (Python defaults to ~1000 frames).
  • Can burrow far from the start before trying nearby alternatives — bad when the target is close.
  • On infinite or enormous implicit graphs it can descend forever without a depth cap (IDDFS fixes this).
  • Pre-order, post-order, and in-order variants answer different questions — easy to pick the wrong one.

Intuition Mental model

Maze exploration with breadcrumbs

You're in a maze with a pocket of breadcrumbs and a ball of string. Follow one corridor as far as it goes, dropping crumbs; at a dead end, rewind the string to the last junction with an untried branch. The string is the recursion stack, the crumbs are the visited set.

  1. Pick a corridor at each junction and keep walking = recurse into the first unvisited neighbor.
  2. Drop a breadcrumb in every cell you enter = mark visited, so you never re-explore.
  3. Dead end (or all crumbs ahead) = base case: rewind the string to the previous junction — that's the function returning.
  4. Back at a junction, take the next untried corridor = the loop over remaining neighbors resumes after the recursive call.
  5. The string's length at any moment = current recursion depth; if the maze is one giant corridor, you need a LOT of string — the stack-overflow risk.

Prep Interview questions

Detect a cycle in a directed graph. Why isn't a plain visited set enough?
A visited node isn't necessarily part of a cycle — you need to know whether it's still on the CURRENT path. Use three colors: white (unseen), gray (on the recursion stack), black (finished). An edge to a gray node is a back edge, and a back edge is exactly a cycle.
Produce a topological order of a DAG using DFS.
Run DFS from every unvisited node, and push each node onto a list at POST-order — when its recursion finishes. Reverse the list. A node finishes only after all its descendants, so reversed finish order respects every edge.
Count islands in a binary grid.
Scan every cell; each unvisited land cell starts one DFS that sinks (marks) its whole component through 4-directional neighbors. The number of DFS launches is the number of islands. Watch grid bounds and mark before or immediately upon visiting.
Convert recursive DFS to iterative. What subtly changes?
Replace the call stack with an explicit stack of nodes. Naive push-all-neighbors visits children in reverse order versus the recursive version, and post-order actions need extra care — push (node, expanded) pairs or a second pass. Marking on push versus on pop changes duplicate behavior, mirroring BFS's enqueue rule.
When is DFS strictly the wrong choice against BFS?
Anything asking for shortest, minimum steps, or nearest on an unweighted graph — DFS may return a wildly longer path. Also very deep graphs with recursion (stack overflow) unless you go iterative or use iterative deepening.

Watch out Common bugs

Stack overflow on deep graphs

Recursive DFS on a million-node chain blows the call stack — Python caps recursion near 1000 frames by default, and other runtimes fail at a few hundred thousand. Production code on unbounded inputs uses the iterative form with an explicit stack.

Visited-check placement

Checking visited only at the call boundary (before recursing) versus at function entry are both fine alone — mixing them, or forgetting one path (like the initial call in a components loop), yields double visits or infinite loops on cycles. Pick one convention: guard at entry is the hardest to get wrong.

Pre-order vs post-order confusion

Acting on a node before recursing (pre-order) versus after (post-order) changes the answer entirely: topological sort requires post-order, path-printing wants pre-order, and subtree aggregation must combine children post-order. The classic symptom is a topological sort that's simply the discovery order — wrong.

Forgetting to unmark in backtracking

Pure reachability keeps nodes visited forever, but backtracking problems (find ALL paths, N-queens) must remove the node from the current path when the call unwinds — leaving it marked silently prunes valid solutions.

One screen Cheat sheet

Go deep first, backtrack at dead ends — a stack (usually the call stack) plus a visited set.

Time O(V + E) · Space O(V) worst (recursion/stack depth)
  • Stack (LIFO) drives depth-first order — recursion is just the implicit version.
  • No shortest-path guarantee: DFS finds A path, BFS finds the fewest-hops path.
  • Post-order finish times reversed = topological sort; back edge to an on-stack node = cycle.
  • Space tracks path DEPTH; BFS space tracks frontier WIDTH — choose by graph shape.
  • Iterative form with an explicit stack avoids recursion limits on deep graphs.
  • White/gray/black coloring separates 'seen' from 'still on the current path'.
Pick whenYou need exhaustive exploration, backtracking, topological order, cycle detection, or components — and path length doesn't matter.
Avoid whenYou need shortest/nearest answers (use BFS), or the graph is extremely deep and you're stuck with bounded recursion.

Breadth-First Search (BFS)