A Bit of History
Long before computers existed, French mathematician Charles Pierre Trémaux described a method in the 1880s for escaping an arbitrary maze without a map: mark every passage you walk through, never re-enter a passage that has been marked twice, and when every passage from your current junction has already been tried, backtrack the way you came. That is, structurally, exactly depth-first search — a stack (your memory of "the way back") replacing a physical trail of chalk marks. The algorithm was formalized for graphs in the 20th century and became a cornerstone of computer science through the work of researchers like John Hopcroft and Robert Tarjan, whose 1970s papers on DFS-based algorithms (finding bridges, articulation points, and strongly connected components — all previewed here, formalized in Part 8) are still taught essentially unchanged today.
Analogy: Exploring a Cave System
Imagine exploring an unmapped cave with a ball of string. At every junction, you pick one unexplored tunnel and keep going, unspooling string behind you. When you hit a dead end (or a tunnel you've already fully explored), you reel the string back to the last junction with an unexplored option, and try the next one. You never leave a junction until every tunnel from it has been tried. That's exactly DFS — the "string" is the call stack (or an explicit stack in the iterative version).
Working Principle
Depth-First Search explores as far as possible along each branch before backtracking. Unlike BFS's queue, DFS uses a stack (LIFO) — either explicitly, or implicitly via function-call recursion. Starting from a source vertex, DFS visits it, then recursively visits an unvisited neighbor, then that neighbor's unvisited neighbor, and so on — only backing up (popping the stack) once a dead end (no unvisited neighbors) is reached.
def dfs_pseudocode(graph, u, visited=None):
"""
Precondition: `graph` maps each vertex to a list of neighbors.
Postcondition: every vertex reachable from u has been visited exactly once.
"""
if visited is None:
visited = set()
visited.add(u)
for v in graph[u]:
if v not in visited:
dfs_pseudocode(graph, v, visited) # recursive call = implicit stack push
return visited
Interactive Demo
The same 6-node graph from the BFS deep dive — step forward past the BFS scenes to see DFS explore it instead. Notice how DFS commits to node D (a dead end) before ever touching C, while BFS visited both at the same "layer."
BFS vs DFS — Live Comparison
Click Next repeatedly to pass through all BFS scenes into the DFS scenes on the same graph.
Discovery/Finish Times & Edge Classification
A refinement that unlocks most of DFS's advanced applications: stamp each vertex with a discovery time \(d[v]\) (when it's first visited) and a finish time \(f[v]\) (when the recursive call over all its neighbors returns). Every edge \((u,v)\) encountered during the search then falls into exactly one of four categories:
| Edge type | Condition | Meaning |
|---|---|---|
| Tree edge | \(v\) is unvisited when reached from \(u\) | part of the DFS forest itself |
| Back edge | \(v\) is an ancestor of \(u\) in the DFS tree, still "in progress" | signals a cycle (Part 8) |
| Forward edge | \(v\) is a finished descendant of \(u\) (directed graphs only) | a "shortcut" already covered by tree edges |
| Cross edge | \(v\) is already finished and not a descendant (directed graphs only) | connects unrelated branches or subtrees |
Why This Matters: Cycle Detection in One Line
A directed graph is acyclic (a DAG, Part 7) if and only if DFS finds no back edges. This single classification — checking whether a neighbor is "currently on the recursion stack" versus "already fully finished" — is the entire basis of cycle detection, topological sorting, and strongly connected component algorithms (Kosaraju's and Tarjan's, coming in a later batch of deep dives).
Complexity Analysis
Identical to BFS's argument, just with a stack instead of a queue: each vertex is pushed and popped exactly once, and each edge is examined at most twice (undirected) or once (directed).
$$\text{Time: } O(V + E) \qquad \text{Space: } O(V) \text{ for the visited set and the recursion/explicit stack}$$
Recursion Depth Is a Real Constraint
Recursive DFS on a graph with a long path (e.g., a 100,000-node path graph \(P_n\)) can exceed the call-stack limit in languages like Python (default recursion limit: 1000) or cause a stack overflow in C++/Java on very deep graphs. The iterative version below, using an explicit stack on the heap instead of the call stack, sidesteps this entirely — the same tradeoff that motivates iterative rewrites of any deeply recursive algorithm.
Implementation
Real-World Applications
Solving a Maze with DFS (and Why It's Not Always Shortest)
Model each maze cell as a vertex and each legal move as an edge, then run DFS from the entrance. DFS is guaranteed to find an exit if one exists (it explores every reachable cell), but unlike BFS, the path it finds is not guaranteed to be the shortest — it may wander down long dead-end corridors before finally reaching the exit. This is the exact same dead-end-then-backtrack behavior Trémaux described in the 1880s, just running on a computer instead of a person with a ball of string.
Beyond mazes, DFS underlies: topological sorting (Part 7 — order tasks so dependencies come first, using reverse finish-time order), cycle detection in build systems and package managers (detecting circular dependencies), flood fill in image editors (the "paint bucket" tool), and finding connected components, bridges, and articulation points — critical infrastructure and network-reliability analysis (Part 8).
Exercises
- Trace DFS by hand on the graph \(A\text{-}B, B\text{-}C, C\text{-}A, C\text{-}D\) starting at \(A\), listing discovery and finish times for every vertex.
- Explain why a back edge can only exist in the direction "descendant discovers an ancestor still on the stack," never the reverse.
- Modify the iterative DFS code above to also record each vertex's parent, then write a function to reconstruct the path from the source to any visited vertex.
- Challenge: Adapt DFS to detect a cycle in a directed graph using three vertex states (white/unvisited, gray/on-stack, black/finished) instead of a single visited set, and explain why a single boolean visited flag is insufficient for directed cycle detection (hint: consider a cross edge to an already-finished vertex versus a back edge to a gray vertex).
Limitations
DFS Does Not Find Shortest Paths
Just as BFS cannot handle weighted edges, DFS cannot guarantee shortest paths even in unweighted graphs — its exploration order depends entirely on neighbor-list ordering, not distance. Use DFS when you need to explore everything reachable, detect structure (cycles, components, ordering), or backtrack through a search space — use BFS or Dijkstra's algorithm when you specifically need shortest paths.