DFS Depth-First Search
Dive as deep as possible along each path before backtracking β the go-to traversal for cycle detection, topological sort, connected components, and any problem requiring exhaustive path exploration.
What is DFS?
Depth-First Search picks one neighbor and follows it as far as possible before backing up and trying the next β it uses recursion (or an explicit stack) to remember where to resume after backtracking. While BFS explores in concentric rings, DFS plunges down a single path until it hits a dead end, then reverses course. This depth-first behavior gives DFS its three killer applications: cycle detection (a back edge to an ancestor means a cycle), topological sorting (reverse post-order gives a valid ordering of dependencies), and connected component discovery (each DFS call from an unvisited node is a new component). DFS runs in O(V + E) time, visiting every vertex and examining every edge exactly once. The recursive version mirrors the call stack naturally β each recursive call represents "enter this node," and each return represents "backtrack from this node." For graphs with thousands of nodes, an iterative version with an explicit Deque<Integer> avoids StackOverflowError. Unlike BFS, DFS does NOT guarantee shortest path β it finds a path, not necessarily the shortest one.
How It Thinks
DFS β Step by Step
- Code placed BEFORE the recursive calls runs when you first discover a node (pre-order).
- Code placed AFTER runs when you've fully processed all descendants (post-order). Pre-order gives discovery order.
- Post-order is the foundation of topological sort β a node finishes only after all its dependencies have finished.
- Unlike BFS where you mark before enqueueing, iterative DFS may push the same node multiple times (from different neighbors).
- The
if (visited) continueafter pop handles this. - Alternatively, mark before pushing β but then the visit order differs slightly from recursive DFS.
visited set for cycle detection in directed graphs. A boolean set only tells you "have I seen this node" β but in directed graphs, a visited node might be fully processed (BLACK), not part of the current path. Only GRAY nodes (currently on the recursion stack) indicate a cycle. For undirected graphs, a simpler "visited + parent" check suffices.
Cycle Detection β Directed vs Undirected
Cycle detection in undirected graphs and directed graphs require different techniques. In an undirected graph, a cycle exists if DFS encounters a visited neighbor that is NOT the parent it came from β since every edge is bidirectional, the node you arrived from always appears as "visited," so you must exclude it. In a directed graph, a cycle exists only when DFS encounters a GRAY node (still on the current DFS path) β a BLACK node was processed on a different path and does not form a cycle with the current path. The Course Schedule problem (LC 207) is the canonical directed cycle detection problem.
visited + parent check
three-color (WHITE/GRAY/BLACK)
- Directed: "visited neighbor is GRAY." These are the two cycle detection formulas.
- In undirected, every edge is bidirectional, so the parent always appears visited β exclude it.
- In directed, an edge to a BLACK node is not a cycle β it's a cross/forward edge to a different, already-completed DFS subtree.
Topological Sort β Ordering Dependencies
A topological sort of a Directed Acyclic Graph (DAG) is a linear ordering of vertices such that for every directed edge uβv, u appears before v. It answers: "in what order should I process tasks so that all dependencies are satisfied before any task that depends on them?" Build systems (Maven, Gradle), course prerequisite planning, and package installation order all reduce to topological sort. DFS gives topological sort for free: run DFS, add each node to a list when it finishes (post-order), then reverse the list.
- Instead of DFS + reverse post-order, repeatedly remove nodes with in-degree 0 and add them to the result.
- Both give valid topological orders in O(V+E).
- Kahn's naturally detects cycles (if the result has fewer than V nodes, a cycle exists).
- Use Kahn's when you need to detect cycles simultaneously or when the problem gives in-degree information directly.
Connected Components
A connected component is a maximal set of vertices where every vertex is reachable from every other vertex. Finding connected components is a direct application of the outer DFS loop: iterate over all vertices, and for each unvisited vertex, launch a DFS that marks all reachable vertices β each launch is one component. This pattern powers "Number of Islands" (LC 200), "Number of Provinces" (LC 547), and "Accounts Merge" (LC 721). For dynamic connectivity (where edges are added/removed over time), Union-Find is more efficient, but for static graphs, DFS is the standard approach.
- Each '1' cell is a vertex, edges connect adjacent '1' cells. Each DFS from an unvisited '1' marks one island.
- Count launches = count islands. The grid is the graph β no adjacency list needed.
DFS on 2D Grids β Flood Fill Pattern
Grid DFS is the flood fill algorithm: start at a cell, recursively visit all 4-directional neighbors that match some criterion, mark as visited. This handles "Number of Islands" (mark connected '1' cells), "Surrounded Regions" (mark 'O' cells reachable from the border), and "Pacific Atlantic Water Flow" (DFS from both oceans inward). The recursive version is cleaner for grids because the 4-direction expansion maps directly to 4 recursive calls.
- For grid DFS, modifying the grid directly (
grid[r][c] = '0') is the common pattern β it avoids allocating a separate visited array. - If the grid must not be modified, use
boolean[][] visitedor restore the original value after DFS (backtracking). - In interviews, clarify whether mutation is allowed.
Build It Once
Build this from scratch once β it makes the mechanics concrete. DFS is boilerplate: visited set + recursive neighbor loop. Memorise the template and the three-color variant.
Use It In Java
Deque<Integer> stack = new ArrayDeque<>() β use push() / pop() import java.util.*; | Pattern | Complexity | Note |
|---|---|---|
visited.add(node) | O(1) | HashSet β returns false if already present |
stack.push(node) / stack.pop() | O(1) | ArrayDeque β iterative DFS stack |
int[] color = new int[n] | O(1) access | Three-color scheme: 0=W, 1=G, 2=B |
boolean[] visited | O(1) access | Faster than HashSet for 0-indexed graphs |
map.getOrDefault(k, List.of()) | O(1) | Safe neighbor lookup β avoids null |
grid[r][c] = '0' | O(1) | In-place visited marking for grid DFS |
-Xss4m JVM flag | β | Increase stack size for deep recursive DFS |
StackOverflowError. LeetCode's judge has a larger stack, but local testing will crash on deep graphs. For graphs with V > 10,000, convert recursive DFS to iterative with an explicit ArrayDeque stack β same O(V+E) time, O(V) heap space instead of stack space.
boolean[] over HashSet when vertices are 0-indexed integers. Problems To Solve
DFS interview problems cluster into four patterns: connected components counting (launch DFS from each unvisited node), cycle detection (undirected = parent check, directed = three-color), topological ordering (reverse post-order on DAGs), and backtracking (enumerate all valid configurations using DFS + undo). Recognise the DFS signal: "detect cycle," "connected components," "dependency order," "all paths," or "flood fill" β these are almost always DFS, never BFS.
| Difficulty | Pattern | Problem | Key Insight |
|---|---|---|---|
| Medium | dfs | Number of Islands β LC 200 | Grid DFS flood fill: each launch from an unvisited '1' marks one island. Count launches = count islands. Mark visited by setting cell to '0'. |
| Medium | dfs | Course Schedule β LC 207 | Model courses as nodes, prerequisites as directed edges. Run three-color DFS: if any neighbor is GRAY during DFS, a cycle exists and not all courses can be finished. |
| Medium | dfs | Course Schedule II β LC 210 | Same as LC 207, but instead of detecting a cycle, collect the topological order. DFS with reverse post-order. If a cycle exists, return empty. |
| Medium | dfs | Clone Graph β LC 133 | DFS with a HashMap mapping originalβclone. When visiting a neighbor, check the map: if cloned, reuse; if not, create and recurse. The map prevents infinite loops on cycles. |
| Medium | dfs | Pacific Atlantic Water Flow β LC 417 | Two grid DFS passes: one from Pacific border cells, one from Atlantic border cells. Cells reachable from BOTH passes are the answer. Reverse the flow direction β DFS "uphill" from oceans. |
| Medium | backtrack | All Paths From Source to Target β LC 797 | DFS with path tracking: add current node to path, recurse into neighbors, remove node after returning (backtrack). No visited set needed β the graph is a DAG, so no cycles. |
- DFS is the right choice when the problem requires exploring all possibilities, detecting structural properties (cycles, components), or ordering dependencies.
- If you see "cycle," think three-color DFS.
- If you see "topological order" or "prerequisites," think DFS reverse post-order (or Kahn's BFS).
- If you see "count regions" or "flood fill," think DFS + component counting.
- DFS is recursive by nature β start with the recursive version, convert to iterative only if depth is a concern.