Depth-First Search (DFS)
Explore a graph by following one path as deep as possible before backtracking, using recursion or an explicit stack.
Overview
DFS starts at a source, moves to an unvisited neighbour, and keeps going deeper until it hits a dead end; then it backtracks to the most recent node that still has an unexplored neighbour. The natural implementation is recursion; the call stack is the Stack of the path currently being explored.
Unlike Breadth-First Search (BFS), DFS does not find shortest paths, but it exposes structure that BFS cannot: discovery and finishing times, tree/back/forward/cross edge classification, and a natural post-order. That structure underpins Topological Sort, Cycle Detection, Tarjan's SCC Algorithm, Bridges and Articulation Points.
Intuition
A mental model before the formal terms.
Walking a maze with a ball of string: at each junction you take an untried corridor, unrolling string behind you. At a dead end you rewind the string to the previous junction and try another corridor. The string is the recursion stack; the chalk marks on visited junctions are the visited set. You eventually see every reachable room, but the first route you find to the exit is rarely the shortest.
How it works
- Mark
uvisited and record its discovery (pre-order) time. - For each neighbour
vofu: if unvisited, setparent[v] = uand recurse intov. If already visited and still on the stack (in a directed graph),u → vis a back edge — a cycle. - After all neighbours are processed, record the finish (post-order) time of
u. Reverse finishing order is a topological order on a DAG. - Iterative version: push
s; popu; if unvisited, mark it and push all its unvisited neighbours. To reproduce recursive pre-order exactly, push neighbours in reverse order; to get post-order, keep an iterator index per stack frame instead of popping nodes.
Why it works
Every node is marked before recursion into it and never unmarked, so each node is entered once and each adjacency list scanned once: O(V + E).
The white-path theorem: v becomes a descendant of u in the DFS tree iff at the moment u is discovered there is a path from u to v through undiscovered nodes. This is what makes finishing times meaningful for topological sorting and SCCs.
Parenthesis structure: for any two nodes the intervals [discover, finish] are either nested or disjoint, never partially overlapping — exactly what a stack produces.
Recognition
How to tell a problem wants this.
- You need to know whether a path exists, not how long it is (reachability, flood fill, count components).
- The problem talks about cycles, dependencies, ordering, "can all courses be finished".
- You must enumerate all paths or all configurations — DFS with undo is Recursion & Backtracking/backtracking.
- Tree problems where each node needs information from its subtree (post-order): heights, diameters, subtree sums.
Interactive visualization
Play, step, change the input. ← → and space work too.
1stack = [source]; visited = {}; order = []2while stack not empty:3 u = stack.pop()4 if u in visited: continue5 visited.add(u); order.append(u)6 for v in reversed(neighbors(u)):7 if v not in visited: stack.push(v)Pseudocode
1def dfs(u):2 visited.add(u); pre.append(u)3 for v in adj[u]:4 if v not in visited:5 parent[v] = u; dfs(v)6 post.append(u)7for s in nodes: if s not in visited: dfs(s)Implementations
1import sys2 3 4def dfs(adj: list[list[int]], s: int) -> tuple[list[int], list[int], list[int]]:5 """Recursive DFS. Returns (pre-order, post-order, parent)."""61 · State7 sys.setrecursionlimit(max(10_000, len(adj) + 100))8 n = len(adj)9 visited = [False] * n10 parent = [-1] * n11 pre: list[int] = []12 post: list[int] = []13 142 · Recursive visit15 def run(u: int) -> None:16 visited[u] = True17 pre.append(u)183 · Explore neighbours19 for v in adj[u]:20 if not visited[v]:21 parent[v] = u22 run(v)234 · Post-order24 post.append(u)25 26 run(s)27 return pre, post, parent28 29 305 · Iterative pre-order (same order as recursive, no stack-depth limit)31def dfs_iterative(adj: list[list[int]], s: int) -> list[int]:32 seen = [False] * len(adj)33 order: list[int] = []34 stack = [s]35 while stack:36 u = stack.pop()37 if seen[u]:38 continue39 seen[u] = True40 order.append(u)41 for v in reversed(adj[u]): # reversed so the first neighbour is popped first42 if not seen[v]:43 stack.append(v)44 return ordersys.setrecursionlimitraises the default limit of 1000 so a path graph of a few thousand nodes does not crash.runis a nested function that closes over the outer lists; it needs nononlocalbecause it only mutates, never rebinds.- Pre-order appends before the loop, post-order after — the same shape as the C++ and JS versions.
dfs_iterativeusesreversed(adj[u])so neighbours are popped in original order; nodes already seen are skipped at pop time.
Python frames are heavy (~500 bytes+) and the interpreter C stack can segfault around 10^5 frames even after raising the limit; prefer dfs_iterative beyond a few thousand nodes.
sys.setrecursionlimitonly changes the interpreter guard, not the OS stack; usethreading.stack_sizeor iteration for really deep graphs.- A
listis Python's native stack:append/pop()are amortised O(1). reversed()returns an iterator, so no copy of the neighbour list is made.
- Forgetting
sys.setrecursionlimitand gettingRecursionErroron a 1000-node chain. - Rebinding a closed-over list (
pre = pre + [u]) inside the nested function, which needsnonlocaland copies. - Marking seen at push time in the iterative version.
- Recursion limits differ sharply: Python defaults to 1000 frames (raise with
sys.setrecursionlimit), JS/TS engines allow ~10k, C++ depends on the OS stack (~10^5–10^6). The iterative version is the portable safe choice. - C++ needs a struct or reference parameters to share state; JS/TS/Python use closures over outer variables.
- Stack primitive: C++
std::stack(top/pop are separate), JS/TS arrays (push/pop), Python lists (append/pop).
Complexity
Recursion depth can reach V on a path graph — Python's default limit of 1000 and typical JS engines (~10k frames) overflow; use the iterative form for n ≥ 10^4.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Reachability, flood fill, counting Connected Components — any "does a path exist" question.
- Anything needing post-order: DFS Topological Sort, subtree aggregation, Tarjan's SCC Algorithm, Bridges.
- Cycle detection in directed graphs via the three-colour (white/grey/black) scheme.
- Enumerating all paths or combinations with backtracking, where BFS would have to store every partial path.
- Shortest path by edge count — DFS finds *a* path, not the shortest; use Breadth-First Search (BFS).
- Extremely deep graphs with recursion (long chains, big grids): stack overflow. Switch to the explicit-stack version.
- Nearest-goal search in a huge implicit space: DFS may wander down an infinite or enormous branch; BFS or iterative deepening is safer.
Alternatives
Common mistakes
- Iterative DFS that marks visited at push time produces a different (and sometimes wrong for post-order) order than recursive DFS — mark at pop time and skip already-visited pops.
- In an undirected graph, treating the edge back to the parent as a cycle. Skip
v == parent[u](or, with multi-edges, skip by edge id). - Using a plain visited flag for directed cycle detection — you need "on the current stack" (grey) versus "finished" (black).
- Forgetting
sys.setrecursionlimitin Python, or relying on recursion for a 1000×1000 grid. - Mutating the shared adjacency list during traversal.
Interview patterns
- Grid flood fill: number of islands, max area of island, surrounded regions.
- Directed cycle detection with colours: course schedule, detect deadlock.
- Post-order aggregation on trees: diameter, max path sum, subtree sizes (Tree DP).
- Backtracking enumeration: subsets, permutations, word search — DFS over an implicit state tree with undo.
- Clone graph: DFS with a hash map from original node to its copy.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Number of IslandsIntermediate
- Word SearchAdvanced