Trees & Graphs
Intermediate 10 min read

Depth-first search

In one line

DFS follows one path as deep as it can before backing up, which makes it the natural tool for exploring regions, finding connected components and detecting cycles.

What it is

Depth-first search starts at a node, moves to an unvisited neighbour, then to that node's unvisited neighbour, and keeps going until it hits a dead end. Then it backtracks to the most recent node that still has unexplored neighbours. It is written with recursion (the call stack) or an explicit stack.

DFS visits every reachable node once, so it costs O(V + E) time, the same as BFS. It does not find shortest paths, but it naturally answers questions about structure: which nodes are connected, does a cycle exist, what order must things finish in, and what are all the paths.

In directed graphs, DFS tracks three states per node: unvisited, on the current path (in progress), and finished. Meeting a node that is still on the current path means you found a back edge, which is exactly a cycle.

Why interviewers ask

Number of islands, flood fill, clone graph, course schedule (cycle detection), connected components, path existence and every backtracking problem are DFS. Interviewers look for a clean visited strategy, correct cycle detection in directed vs undirected graphs, and awareness of recursion depth limits.

How it works

  1. 1Mark the current node visited.
  2. 2For each neighbour that is not visited, recurse into it (or push it onto a stack).
  3. 3When all neighbours are done, the call returns: this is the moment the node is finished, which postorder-style algorithms (like topological sort) use.
  4. 4To cover a whole graph, loop over every node and start a new DFS from each one that is still unvisited. Each start is a new connected component.
  5. 5Directed cycle check: colour nodes 0 (unvisited), 1 (on the current path) and 2 (finished). Reaching a colour-1 node means a cycle.
  6. 6Undirected cycle check: a visited neighbour that is not the node you came from (the parent) means a cycle.

Worked example · Counting islands in a 3 by 3 grid

  1. Grid rows: [1 1 0], [0 1 0], [0 0 1]. 1 is land, 0 is water; land connects up, down, left and right.
  2. Scan from (0,0). It is land: islands = 1. DFS sinks (0,0), then its neighbour (0,1), then from (0,1) goes down to (1,1). (1,1) has no unvisited land neighbours, so DFS backs up and ends.
  3. Continue scanning: (0,1) and (1,1) are now water, so they are skipped, as are the 0 cells.
  4. (2,2) is land: islands = 2. Its neighbours (1,2) and (2,1) are water, so the DFS ends immediately.
  5. Scan finishes. Answer: 2 islands. Each cell was visited at most a constant number of times: O(9) work.

Picture it

DFS is exploring a cave system with a ball of string: you keep taking the next unexplored tunnel until you hit a dead end, then follow the string back to the last junction with an unexplored tunnel.

Reference code

def num_islands(grid):
    rows, cols = len(grid), len(grid[0])
    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] != '1':
                continue
            count += 1
            grid[r][c] = '0'          # sink it so it is never counted again
            stack = [(r, c)]
            while stack:
                i, j = stack.pop()
                for ni, nj in ((i+1, j), (i-1, j), (i, j+1), (i, j-1)):
                    if 0 <= ni < rows and 0 <= nj < cols and grid[ni][nj] == '1':
                        grid[ni][nj] = '0'
                        stack.append((ni, nj))
    return count

def has_cycle(n, adj):
    state = [0] * n                   # 0 new, 1 on current path, 2 done
    def dfs(u):
        state[u] = 1
        for v in adj[u]:
            if state[v] == 1 or (state[v] == 0 and dfs(v)):
                return True           # back edge found
        state[u] = 2
        return False
    return any(state[u] == 0 and dfs(u) for u in range(n))

num_islands uses an explicit stack so deep regions cannot overflow Python's recursion limit, and marks cells when pushed. has_cycle is the three-colour check for directed graphs. Both are O(V + E); for the grid that is O(rows x cols).

Key patterns

Flood fill and counting regions

When: Number of islands, max area of island, surrounded regions

How: Loop over cells; on each unvisited land cell, increment the count and DFS to mark the whole region.

Connected components

When: Count groups, check if a graph is connected or a valid tree

How: Start DFS from each unvisited node; the number of starts is the number of components. A valid tree has one component and V - 1 edges.

Directed cycle detection with colours

When: Course schedule, deadlock detection, dependency validation

How: States unvisited, in progress, done. An edge to an in-progress node is a cycle.

Backtracking

When: All paths, permutations, word search on a grid

How: DFS that undoes its choice (unmarks the node) when returning, so other paths can reuse it.

Real-world use

Garbage collection marking

Mark-and-sweep collectors in runtimes like the JVM and Go traverse every object reachable from the roots, typically depth-first using a mark stack, then free everything left unmarked.

Deadlock detection

PostgreSQL and MySQL InnoDB build a wait-for graph of transactions and search it for cycles; a cycle means a deadlock, and one transaction is rolled back to break it.

Build tools and module bundlers

Webpack and other bundlers walk import graphs depth-first from the entry file to collect every module, and report circular dependencies when the walk returns to a module still in progress.

Image editors

The paint bucket tool in image editors is flood fill: a DFS (or BFS) from the clicked pixel that recolours every connected pixel of the same colour.

In a project

Your team's microservices call each other, and a recent outage was caused by a circular call chain. You are asked to build a check that runs in CI and blocks deployments that introduce call cycles.

  1. Collect declared dependencies from each service's config into a directed graph: service to the services it calls.
  2. Run DFS with three colours from every unvisited service.
  3. Keep the current path on a stack; when DFS meets a node coloured in-progress, slice the stack from that node to report the exact cycle, for example orders to payments to orders.
  4. Also report services unreachable from any public gateway (DFS from gateways, list unvisited nodes) as candidates for removal.
  5. Run the check on every pull request; it takes O(V + E) and finishes in milliseconds.

Impact: Circular dependencies are caught before deployment with a readable cycle path, preventing a whole class of cascading-timeout outages, and the dead-service report gives cleanup targets.

Trade-offs

Use it when

  • You need to explore everything reachable: regions, components, reachability
  • Cycle detection and dependency ordering
  • Enumerating all paths or configurations (backtracking)
  • Memory matters on wide graphs: DFS keeps only the current path, O(depth)

Avoid it when

  • You need the shortest path in an unweighted graph: use BFS
  • The graph can be very deep and you are using recursion in Python (default limit about 1000): use an explicit stack
  • The search space is infinite or huge and the answer is shallow: DFS can wander deep; use BFS or iterative deepening

Cost: O(V + E) time. Space O(V) for the visited set plus O(depth) for the stack, which can reach O(V).

Common mistakes

  • Using the undirected cycle rule (visited and not parent) on a directed graph, or a plain visited set for directed cycles; you need the in-progress state.
  • Forgetting to start DFS from every unvisited node, so disconnected parts are never explored.
  • Hitting RecursionError on large grids (a 300 by 300 grid of land is 90,000 deep) instead of using an explicit stack.
  • Mutating the input grid without saying so; mention it or use a separate visited set.
  • Claiming DFS finds shortest paths.

Interview questions

Quick revision

  • Go deep, backtrack at dead ends; recursion or explicit stack.
  • O(V + E) time; O(depth) stack space.
  • Count components: number of DFS starts over all unvisited nodes.
  • Directed cycle: three colours, edge to in-progress node = cycle.
  • Undirected cycle: visited neighbour that is not the parent.
  • DFS does not give shortest paths; BFS does (unweighted).
  • Python recursion limit about 1000: use an explicit stack for big inputs.

Check yourself

1. In a directed graph, DFS reaches a node that is already visited but has finished (state done). What does that mean?

2. Grid rows [1 0 1], [0 1 0], [1 0 1] with 4-directional connections. How many islands?

3. What is a common problem with recursive DFS in Python on a 1000 by 1000 grid full of land?

4. Which task is DFS the wrong default choice for?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free