Depth-First Search
Walk a graph by going as deep as you can, then backing up. Write DFS with recursion or a stack, and use it to count connected pieces and islands in a grid.
On this page
You are exploring a cave with a ball of string tied to the entrance. At every fork you take the first passage you have not tried and keep going, unrolling string behind you. At a dead end, or where every passage leads somewhere you have been, you wind the string back to the last fork with an untried passage and take it. When the string is fully wound back at the entrance, you have seen the whole cave.
That is depth-first search (DFS). Where BFS spreads out in rings, DFS commits to one route and follows it as deep as it goes before trying anything else.
The idea: go deep, then back up
The string is a record of how to get back. In code, recursion gives you that record for free: each call waits on the call stack while its child explores, exactly as in Recursion. A node's DFS is:
- Mark the node seen.
- For each neighbour that is not seen yet, run DFS on it, all the way, before looking at the next neighbour.
The seen mark stops you from going round a cycle forever, as it did in BFS.
Trace it on the same bus map
This is the bus map from Graph Basics again, with the same alphabetical neighbour lists and the same start, A. The labels show visit order, and the highlighted edges are the string.
dfs(node): mark node seen for each neighbour next of node: if next not seen: dfs(next) return- call stack0A
1Call
dfs(A). Mark A seen. It is visit #1. - call stack0A1B
2A's first neighbour is B, unseen. Go to B right away. A is not finished; it waits on the call stack.
- call stack0A1B2D
3B's first neighbour, A, is seen: skip. Next is D, unseen: go deeper.
- call stack0A1B2D3C
4D's first neighbour, B, is seen. C is not, so go to C. BFS reached C in one step from A; DFS gets there the long way round.
- call stack0A1B2D3C4E
5C: A and D are seen. E is unseen, so go to E.
- call stack0A1B2D3C4E5F
6E: C is seen. F is unseen, so go to F.
- call stack0A1B2D3C4E5F6G
7F: D and E are seen. G is unseen, so go to G. The stack is now 7 calls deep.
- call stack0A1B2D3C4E5F
8G's only neighbour, F, is seen. Dead end.
dfs(G)returns, and we are back inside F's loop. - call stack0A1B2D3C4E
9G was F's last neighbour. F has nothing left to try, so
dfs(F)returns. We are back inside E's loop. - call stack0A1B2D3C
10E's neighbours, C and F, are both seen.
dfs(E)returns to C. - call stack0A1B2D
11C has checked A, D and E. Nothing new, so
dfs(C)returns to D. - call stack0A1B
12D still has F on its list, but F was reached through E long ago. Skip it.
dfs(D)returns to B. - call stack0A
13B has checked A and D.
dfs(B)returns to A. Each return is the string being wound back one fork. - call stack
- visited
- 7
14Back in A, its last neighbour C is seen.
dfs(A)returns and the stack is empty. Every stop was visited once. - call stack
- DFS route to G
- 6 edges
- BFS route to G
- 4 edges
15Visit order: A, B, D, C, E, F, G. BFS gave A, B, C, D, E, F, G. The highlighted edges form the DFS tree: its route from A to G uses 6 edges, while BFS found one with 4.
Both searches visit every stop once. Each draws a tree on the graph: for every node, the edge it first arrived by. The trees have very different shapes.
The code
// graph: adjacency list, e.g. { A: ["B", "C"], B: ["A", "D"], ... }
function dfs(graph, start) {
const seen = new Set();
const order = [];
function visit(node) {
seen.add(node);
order.push(node);
for (const next of graph[node]) {
if (!seen.has(next)) visit(next);
}
}
visit(start);
return order;
}
// dfs(graph, "A") → ["A", "B", "D", "C", "E", "F", "G"]# graph: adjacency list, e.g. {"A": ["B", "C"], "B": ["A", "D"], ...}
def dfs(graph, start):
seen = set()
order = []
def visit(node):
seen.add(node)
order.append(node)
for nxt in graph[node]:
if nxt not in seen:
visit(nxt)
visit(start)
return order
# dfs(graph, "A") → ['A', 'B', 'D', 'C', 'E', 'F', 'G']You can replace the call stack with a stack of your own. Pop a node, skip it if it is already seen, otherwise mark it and push its neighbours. Push them in reverse so the first neighbour comes off the stack first; then the order matches the recursive version.
function dfsIterative(graph, start) {
const seen = new Set();
const order = [];
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (seen.has(node)) continue; // pushed twice, already handled
seen.add(node);
order.push(node);
const nbrs = graph[node];
for (let i = nbrs.length - 1; i >= 0; i--) {
if (!seen.has(nbrs[i])) stack.push(nbrs[i]);
}
}
return order;
}def dfs_iterative(graph, start):
seen = set()
order = []
stack = [start]
while stack:
node = stack.pop()
if node in seen:
continue # pushed twice, already handled
seen.add(node)
order.append(node)
for nxt in reversed(graph[node]):
if nxt not in seen:
stack.append(nxt)
return order| Approach | Time | Space | Why |
|---|---|---|---|
| Recursive DFS | O(V + E) | O(V) | Each node is visited once and its list read once. The seen set and the call stack can each reach V. |
| Iterative DFS | O(V + E) | O(V + E) | A node can be pushed once per edge that leads to it, so the stack can hold up to E entries. |
Counting islands in a grid
The most common use of DFS is counting connected components: the separate pieces of a graph. Scan every node. Whenever you find one that is not seen yet, you have found a new piece. Count it, then run DFS from it to mark the whole piece seen, so you never count it again.
On a grid this is the "islands" question: land cells (#) that touch up, down, left or right belong to the same island. The DFS that marks an island is often called a flood fill, like the paint bucket in a drawing app.
// grid: array of strings. "#" is land, "." is water.
function countIslands(grid) {
const rows = grid.length, cols = grid[0].length;
const seen = grid.map((row) => new Array(row.length).fill(false));
function fill(r, c) {
if (r < 0 || r >= rows || c < 0 || c >= cols) return; // off the map
if (grid[r][c] !== "#" || seen[r][c]) return; // water, or done
seen[r][c] = true;
fill(r - 1, c); fill(r + 1, c); fill(r, c - 1); fill(r, c + 1);
}
let islands = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === "#" && !seen[r][c]) {
islands++; // a piece nobody has reached yet
fill(r, c); // mark all of it
}
}
}
return islands;
}
countIslands(["##..#", "#...#", "..#..", "##..."]); // 4# grid: list of strings. "#" is land, "." is water.
def count_islands(grid):
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
def fill(r, c):
if not (0 <= r < rows and 0 <= c < cols):
return # off the map
if grid[r][c] != "#" or seen[r][c]:
return # water, or done
seen[r][c] = True
fill(r - 1, c); fill(r + 1, c); fill(r, c - 1); fill(r, c + 1)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "#" and not seen[r][c]:
islands += 1 # a piece nobody has reached yet
fill(r, c) # mark all of it
return islands
count_islands(["##..#", "#...#", "..#..", "##..."]) # 4The outer loops touch every cell once, and fill marks each land cell once and looks at its 4 sides. Total: O(R × C) time, however many islands there are. BFS would work too; the recursive DFS is shorter to write.
BFS or DFS
| You need | Use | Why |
|---|---|---|
| Fewest edges from A to B | BFS | Visits nodes in order of distance. |
| Can A reach B at all? | Either | Both visit everything reachable. |
| Count pieces, fill a region | Either, DFS is shorter | Only "reached or not" matters. |
| Detect a cycle in a directed graph | DFS | It knows which nodes are on the current path (the call stack). Kahn's algorithm also works. |
Common mistakes
Check yourself
Graph: { P: [Q, R], Q: [P, S], R: [P, S, T], S: [Q, R], T: [R] }. In what order does recursive DFS from P visit the nodes?
How many islands are in the grid ["#.#", ".#.", "#.#"]?
Why does the iterative DFS check seen after popping, even though it also checks before pushing?
What to practise next
DFS has one more trick: when a call returns, everything reachable from that node is finished. Topological Sort uses that moment to put tasks in an order that respects every "must come before" rule. It also shows a queue-based way to do the same job.