DFSDepth-first search
Go as deep as possible then back out; the basis of components and topological sort.
Used for: Walking folders, flood fill, detecting circular dependencies
01Why it exists
Finder, or the du command, has to work out how much space a folder takes up: step into a subfolder, total it up, come back to the parent and add it in, and keep going down until there are no subfolders left.
Why this fitsGo in, finish the work, come back out — that is exactly the recursive structure of DFS. Anything tree-shaped (a file system, the DOM, a JSON document) is almost always walked with DFS.
One click and the entire connected patch of the same colour is filled in. Flood fill in image processing, clearing connected same-coloured tiles in a game, counting islands on a map — all of them are about finding a connected region.
Why this fitsDFS starts at one cell and covers everything it can reach; what it covers is one connected component. The code is shorter than BFS, just a few lines with recursion.
Module A imports B, B imports C, and C imports A again; a bundler has to catch that cycle before it causes trouble. Spreadsheet formulas that reference each other and package version constraints behave the same way.
Why this fitsDFS can tell a node that is still being explored apart from one that is finished. Reaching a node that is still being explored means there is a cycle. BFS cannot make that distinction.
Reach for it when you see:Connected regions, flood fill, whether a cycle exists, all paths or all combinations, walking a tree structure, needing to backtrack.
02The core idea
DFS picks one path and keeps going deeper until there is no road left, then backs up to the last fork, where it takes another route it has not tried. That order — deep first, back up afterwards — is exactly the last-in-first-out behaviour of a stack, which is why recursion is the most natural way to write it: the call stack already is the stack you need.
Compared with BFS, DFS makes no promise about the shortest path, but it does remember how it got to where it is, and that path information is what makes it the foundation for cycle detection, connected components, topological sorting and enumerating every path.
Every node passes through three states: undiscovered, in progress (entered, but its neighbours are not all done, so it is still sitting on the stack) and finished (every neighbour handled, popped off the stack). A great deal of the advanced work rests on that distinction — running into a node that is still in progress, for example, means the graph contains a cycle.
03The algorithm
- 1Call
dfs(u)on the start node, markuas discovered, and record it in the visit order. - 2Look at each neighbour
vofuin turn: ifvhas not been discovered, recurse intodfs(v)immediately, finishing the path throughvbefore coming back for the next neighbour. - 3Once every neighbour of
uhas been seen,dfs(u)returns — it backtracks to the node that called it. - 4When the start node's
dfsreturns, every node reachable from the start has been visited. To cover the whole graph, call it again on each node that is still undiscovered; each of those calls is one connected component.
04Interactive demo
The same graph, again starting at A, with neighbours taken in alphabetical order. Watch the stack grow tall and shrink back, and look at the visit numbers under the nodes: nothing like the level order BFS produced.
05Code
The recursive version sits closest to the idea. On a deep graph it can exceed the recursion limit, and that is when you switch to an explicit stack, the iterative version.
def dfs(adj, start):
# adj: dict[node, list[node]]. Returns the visit order.
order = []
visited = set()
def go(u):
visited.add(u)
order.append(u)
for v in adj[u]:
if v not in visited:
go(v) # finish the path through v before the next neighbour
# this is u's "finished" moment (the backtracking point)
go(start)
return order
def dfs_iterative(adj, start):
# An explicit stack instead of recursion; the order may differ slightly from the recursive version
order, visited = [], set()
stack = [start]
while stack:
u = stack.pop() # take from the top
if u in visited:
continue
visited.add(u)
order.append(u)
for v in reversed(adj[u]): # push in reverse so the first neighbour comes off first
if v not in visited:
stack.append(v)
return order06Practice
- LeetCode 695Max Area of IslandMedium
- LeetCode 133Clone GraphMedium
- LeetCode 797All Paths From Source to TargetMedium
- LeetCode 207Course ScheduleMedium
- LeetCode 547Number of ProvincesMedium