Skip to content

Expert: advanced patterns and interviews · Graphs · topic 43 of 61 · 40 min

Depth-first search (DFS)

Explanation

Depth-first search follows one path as far as it can, then backs up and tries the next branch. It uses recursion or an explicit stack, runs in O(V + E), and is the natural tool for counting connected components and islands, checking whether a path exists, and finding cycles. Python's recursion limit (1000 by default) makes an explicit stack safer on big graphs.

Go deep first, then back up

Depth-first search (DFS) picks a neighbour and keeps going deeper until it reaches a dead end, then backtracks (goes back) to the last vertex that still has an unvisited neighbour. It is the same idea as the recursion and backtracking of Module 5 and the tree traversals of Module 7, with one addition: a visited set, because a graph can lead you back to a vertex you have already seen.

Think of it like this: Think of exploring a large old building. You walk down a corridor and keep taking doors until you hit a room with no new doors. Then you walk back to the last room that had a door you had not tried, and continue. A chalk mark on each room you have entered stops you going round in circles.
python
def dfs(adj, u, visited):
    visited.add(u)
    for v in adj[u]:
        if v not in visited:
            dfs(adj, v, visited)

adj = {0: [1, 2], 1: [0], 2: [0], 3: [4], 4: [3]}
seen = set()
dfs(adj, 0, seen)
print(sorted(seen))   # Output: [0, 1, 2]
Recursive DFS: the call stack remembers where to come back to. Vertices 3 and 4 are in another component.

The same search works with an explicit stack (a Python list with append and pop). This avoids Python's recursion limit. The visiting order is different from the recursive version, which does not matter when you only need to know what is reachable.

python
def dfs_iter(adj, start):
    visited = {start}
    stack = [start]
    order = []
    while stack:
        u = stack.pop()
        order.append(u)
        for v in adj[u]:
            if v not in visited:
                visited.add(v)
                stack.append(v)
    return order

print(dfs_iter({0: [1, 2], 1: [3], 2: [], 3: []}, 0))   # Output: [0, 2, 1, 3]
The last neighbour pushed is popped first, so 2 comes before 1.

Like BFS, DFS looks at every vertex and every edge a constant number of times: O(V + E) time with an adjacency list, and O(V) extra space for the visited set and the stack (or the call stack).

Connected components and islands

To count the separate pieces of an undirected graph, loop over every vertex. Whenever you find one that is not visited yet, you have found a new component: add 1 to the count and run a DFS from it to mark its whole component. The "number of islands" problem is the same on a grid: each unvisited land cell starts a new island, and the DFS marks all land connected to it up, down, left and right.

Cycles in an undirected graph

In an undirected graph, the edge you just came along leads straight back to your parent, which is visited. That is not a cycle. So pass the parent into the DFS: if you meet a visited neighbour that is NOT your parent, you have found a cycle.

python
def has_cycle_undirected(n, edges):
    adj = [[] for _ in range(n)]
    for u, v in edges:
        adj[u].append(v)
        adj[v].append(u)
    visited = [False] * n

    def dfs(u, parent):
        visited[u] = True
        for v in adj[u]:
            if not visited[v]:
                if dfs(v, u):
                    return True
            elif v != parent:
                return True
        return False

    return any(not visited[u] and dfs(u, -1) for u in range(n))

print(has_cycle_undirected(3, [(0, 1), (1, 2)]))          # Output: False
print(has_cycle_undirected(3, [(0, 1), (1, 2), (2, 0)]))  # Output: True

Cycles in a directed graph: three colours

In a directed graph, meeting a visited vertex is not enough. Take 0 → 1, 0 → 2, 1 → 3, 2 → 3 (a diamond). DFS reaches 3 through 1, finishes it, and later meets 3 again through 2. Vertex 3 is visited, but there is no cycle, because you cannot get from 3 back to 0.

The fix is three colours. White means not visited yet. Grey means "on the current path": the DFS has entered it and not yet finished it (it is on the recursion stack). Black means finished. An edge to a GREY vertex points back into the current path, so it closes a cycle. An edge to a black vertex is safe.

Recursion depth in Python

Every recursive call uses a frame on the call stack, and Python stops at a recursion limit, 1000 by default (sys.getrecursionlimit()). A path of 50,000 vertices, or a 300 × 300 grid that is all land, raises RecursionError with recursive DFS. sys.setrecursionlimit() can raise the limit, but going too high can crash the interpreter, and in the browser (Pyodide) the real stack may be smaller. On big inputs, use an explicit stack, or BFS when the order does not matter.

Common mistakes

  • Using a plain visited set to find cycles in a directed graph, which reports a "cycle" on every diamond. Fix: three colours (white, grey, black).
  • In an undirected graph, treating the edge back to the parent as a cycle. Fix: pass the parent and ignore it once.
  • Running DFS only from vertex 0 and missing the other components. Fix: loop over every vertex and start a DFS from each unvisited one.
  • Recursive DFS on a huge graph or grid, ending in RecursionError. Fix: an explicit stack.
  • Changing the input grid (marking land as "0") without saying so. Fix: say it, or keep a separate visited set.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Depth-first search (DFS) · 1/8

Depth-first search

This lesson is about depth-first search: go as deep as you can along one path, then back up and try the next branch.

Prefer a video by a teacher? Find free videos on “depth first search dfs number of islands cycle detection directed graph explained” on YouTube ↗

In real life: Counting flooded zones on a map

During heavy rain, a city's disaster-management team may split a map into a grid and mark each cell as flooded ("1") or dry ("0") from sensor or satellite data. Flooded cells that touch up, down, left or right form one zone, and each zone may need its own rescue team. Counting zones is the number-of-islands problem: scan the grid and start a DFS from every flooded cell not yet seen.

python
grid = ["11000",
        "11010",
        "00011",
        "10000"]
rows, cols = len(grid), len(grid[0])
seen = set()
zones = 0
for r in range(rows):
    for c in range(cols):
        if grid[r][c] == "1" and (r, c) not in seen:
            zones += 1
            stack = [(r, c)]
            seen.add((r, c))
            while stack:
                cr, cc = stack.pop()
                for nr, nc in ((cr + 1, cc), (cr - 1, cc), (cr, cc + 1), (cr, cc - 1)):
                    if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == "1" and (nr, nc) not in seen:
                        seen.add((nr, nc))
                        stack.append((nr, nc))
print(zones)
Shows 3: the top-left block, the cells on the right, and the single cell at the bottom left.

Practice (0/4 done)

Task 1 · Code · Python

Write path_exists(n, edges, s, t) for an UNDIRECTED graph with vertices 0 to n − 1 given as a list of pairs. Return True if you can get from s to t along edges (True when s == t), else False. Use DFS with a stack or recursion. Example: path_exists(3, [(0, 1), (1, 2)], 0, 2) returns True.

Task 2 · Code · Python

Write count_components(n, edges) for an UNDIRECTED graph with vertices 0 to n − 1. Return the number of connected components (an isolated vertex is a component on its own). Example: count_components(5, [(0, 1), (1, 2), (3, 4)]) returns 2.

Task 3 · Code · Python

Write num_islands(grid). grid is a list of lists of the strings "1" (land) and "0" (water). An island is a group of land cells joined up, down, left or right (not diagonally). Return the number of islands. You may change grid. Use an EXPLICIT stack or a queue: one test is a 60 × 60 block of land, which is too deep for plain recursion in Python. An empty grid has 0 islands.

Task 4 · Code · Python

Write has_cycle_directed(n, edges) for a DIRECTED graph with vertices 0 to n − 1. edges is a list of pairs (u, v) meaning u → v. Return True if the graph has a directed cycle (a self-loop u → u counts), else False. Use DFS with three colours: white (new), grey (on the current path) and black (finished). Example: has_cycle_directed(3, [(0, 1), (1, 2), (2, 0)]) returns True.

Quick quiz

Question 1

1.Directed edges 0→1, 0→2, 1→3, 2→3. A DFS from 0 meets vertex 3 a second time. Is there a cycle?

Question 2

2.In three-colour DFS, what does a grey vertex mean?

Question 3

3.What is Python's default recursion limit?

Question 4

4.How do you count the connected components of an undirected graph?

Question 5

5.In undirected cycle detection, DFS at vertex u sees its parent p, which is visited. What does that mean?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Directed edges 0→1, 0→2, 1→3, 2→3. A DFS from 0 meets vertex 3 a second time. Is there a cycle?

Answer: No: 3 is finished (black), not on the current path

Only an edge to a grey vertex (on the current path) closes a cycle. This is a diamond.

In three-colour DFS, what does a grey vertex mean?

Answer: It is on the current path (entered, not finished)

Grey vertices are exactly the ones on the recursion stack right now.

What is Python's default recursion limit?

Answer: 1000

sys.getrecursionlimit() returns 1000 by default, so deep recursive DFS raises RecursionError.

How do you count the connected components of an undirected graph?

Answer: Start a DFS from every unvisited vertex and count the starts

Each new start finds a piece no earlier DFS could reach.

In undirected cycle detection, DFS at vertex u sees its parent p, which is visited. What does that mean?

Answer: Nothing: it is the edge you came along

The edge u–p is the one used to reach u. Only a visited neighbour other than the parent means a cycle.

An undirected graph has 5 edges. What do all the degrees add up to?Related

Answer: 10

Every edge touches two vertices, so it adds 2 to the total: 2 × 5 = 10.

From Graphs: vertices, edges and how to store them

Why use collections.deque instead of a list for BFS in Python?Related

Answer: popleft() is O(1), while list.pop(0) shifts every item, O(n)

Taking from the front of a list moves all other items; a deque is built for both ends.

From Traversals: DFS and BFS

Kahn's algorithm outputs 4 vertices of a 6-vertex graph. What does that tell you?Related

Answer: The graph has a cycle

Vertices on a cycle never reach in-degree 0, so they are left out.

From Topological sort

What does sys.getrecursionlimit() return in a fresh Python session?Related

Answer: 1000

The default limit is 1000 frames; deeper recursion raises RecursionError.

From Recursion: functions that call themselves

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Graphs” before its exam: Topological sort, Shortest paths: Dijkstra and friends, Graph problem patterns.
  2. Module 9 exam: Graphs · 14 questions · 25 min, without help. 6 topics of this module still to finish first.
  3. Then the next module, Tries, union-find and bit manipulation. 61 topics left in the course before the final exam.

← Previous: Breadth-first search (BFS)