Overview
Depth-first search (DFS) explores a graph by going as deep as possible along one path before backtracking. It uses a stack, either the call stack through recursion or an explicit one, and a visited set so each node is processed once.
DFS is the natural tool for exploring everything reachable: counting connected components, flood-filling regions, detecting cycles, topological ordering, and generating all paths. Unlike BFS it does not find shortest paths, but it uses memory proportional to depth rather than width and is usually shorter to write.
You follow one corridor as far as it goes, unrolling string. At a dead end you walk back along the string to the last junction and try an untried corridor. You mark visited junctions so you never explore the same one twice.
When to use it
- Explore all nodes reachable from a start: islands, provinces, regions.
- Detect cycles in directed or undirected graphs.
- Topological sort of a DAG (postorder).
- Enumerate all paths or configurations (backtracking is DFS on a decision tree).
- Tree problems, which are DFS on a special graph.
Problem patterns it solves
Recognize it when: count islands, provinces, or regions; fill an area.
- 200. Number of Islands
- 547. Number of Provinces
- 695. Max Area of Island
- 733. Flood Fill
Recognize it when: regions touching the border are special; start DFS from the edges.
- 130. Surrounded Regions
- 1020. Number of Enclaves
- 417. Pacific Atlantic Water Flow
Recognize it when: can all courses be finished; find safe states.
- 207. Course Schedule
- 802. Find Eventual Safe States
- 1059. All Paths from Source Lead to Destination
Recognize it when: list every path from source to target in a DAG.
- 797. All Paths From Source to Target
- 1443. Minimum Time to Collect All Apples in a Tree
Recognize it when: copy structure with a visited map.
- 133. Clone Graph
- 138. Copy List with Random Pointer
Recognize it when: critical connections whose removal disconnects the graph.
- 1192. Critical Connections in a Network
Where it is used in real software
Mark-and-sweep collectors run a DFS from root references to mark every reachable object; unmarked objects are freed.
Tools like Make and Bazel run DFS to detect circular dependencies and compute build order.
Sudoku solvers, maze generators, and game AI explore choices depth-first with backtracking.
Compilers and linters traverse call graphs and control-flow graphs to find unreachable code.
Key terms
- Visited set
- Prevents revisiting nodes and infinite loops in cyclic graphs.
- Backtracking
- Returning to the previous node when all neighbors are explored.
- Three colors
- 0 = unvisited, 1 = in the current path, 2 = finished. A 1 -> 1 edge means a directed cycle.
- Discovery / finish time
- When a node is first reached and when its exploration completes.
- Back edge
- An edge to an ancestor in the current DFS path, which indicates a cycle.
How it works, step by step
- 1Mark the node visited
Do this before exploring neighbors.
- 2Process the node (preorder work)
Count it, color it, or add it to a path.
- 3Recurse into each unvisited neighbor
Go deep before trying the next neighbor.
- 4Finish the node (postorder work)
Mark it done, add it to a topological order, or remove it from the current path.
- 5Repeat from every unvisited node
This handles disconnected graphs and counts components.
STEP 1Visit A. Neighbors of A: B, C.
Number of islands
grid = ["11000", "11000", "00100", "00011"]
| Scan cell | Value | Action | Islands |
|---|---|---|---|
| (0,0) | 1 | new island: DFS sinks (0,0),(0,1),(1,0),(1,1) | 1 |
| (0,1)..(1,1) | 0 (sunk) | skip | 1 |
| (2,2) | 1 | new island: DFS sinks (2,2) | 2 |
| (3,3) | 1 | new island: DFS sinks (3,3),(3,4) | 3 |
| (3,4) | 0 (sunk) | skip | 3 |
NOWScan cell: (0,0) | Value: 1 | Action: new island: DFS sinks (0,0),(0,1),(1,0),(1,1) | Islands: 1
3 islands. Each DFS marks its whole island as visited, so every cell is processed a constant number of times: O(rows x cols).
Implementation
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 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;} // Iterative DFS avoids recursion limits on big graphsfunction reachable(graph, start) { const visited = new Set([start]); const stack = [start]; while (stack.length) { const node = stack.pop(); for (const next of graph[node]) { if (!visited.has(next)) { visited.add(next); stack.push(next); } } } return visited;} // Directed cycle detection with three colorsfunction canFinish(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); for (const [course, pre] of prerequisites) graph[pre].push(course); const state = new Array(numCourses).fill(0); // 0 new, 1 visiting, 2 done function hasCycle(u) { if (state[u] === 1) return true; // back edge if (state[u] === 2) return false; state[u] = 1; for (const v of graph[u]) if (hasCycle(v)) return true; state[u] = 2; return false; } for (let i = 0; i < numCourses; i++) if (hasCycle(i)) return false; return true;}Complexity and performance
Each node and edge handled once.
Visited set plus recursion depth.
Recursion can reach R x C deep on a snake-shaped region.
Exponential number of paths in the worst case.
Trade-offs
DFS is simpler for reachability, components, and cycles, and uses O(depth) memory. BFS is required for shortest paths in unweighted graphs.
Recursion is concise but can overflow the stack on 10^5+ nodes (especially in JavaScript). Iterative DFS uses an explicit stack.
Sinking cells saves memory but destroys the grid; use a visited array if the input must remain intact.
Variants and related techniques
A visited neighbor that is not the parent indicates a cycle.
Discovery and low-link times find bridges, articulation points, and strongly connected components in O(V + E).
Pass the parent to avoid walking back up in undirected trees.
Common mistakes
- Marking visited after the recursive call.
Fix: Mark before exploring neighbors, or cycles cause infinite recursion.
- Using a visited set for directed cycle detection.
Fix: A single visited set cannot tell the current path from finished nodes; use three states.
- Starting DFS from only one node.
Fix: Loop over all nodes to cover disconnected components.
- Forgetting to copy the path when saving results.
Fix: Push a copy (new ArrayList<>(path), [...path]), not the mutable list.
Interview questions
How do you detect a cycle in a directed graph?
Run DFS with three states. If you reach a node that is currently on the recursion stack (state 'visiting'), you found a back edge, which means a cycle.
Why can't DFS find shortest paths in unweighted graphs?
DFS may reach a node first through a long path. BFS processes nodes in order of distance, so its first visit is always via a shortest path.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| 733. Flood Fill | Easy | Basic grid DFS. |
| 200. Number of Islands | Medium | Components. |
| 547. Number of Provinces | Medium | Adjacency matrix DFS. |
| 130. Surrounded Regions | Medium | Border-first DFS. |
| 207. Course Schedule | Medium | Three-color cycle detection. |
| 417. Pacific Atlantic Water Flow | Medium | Reverse flow from borders. |
| 1192. Critical Connections in a Network | Hard | Tarjan's bridges. |