Graphs· 20 min

Graph DFS

Dive deep with a stack, backtrack when stuck

Setup

Step 1 / 23

Current step

1 / 23

Start DFS from A: push it on the stack.

Step log

Current
On stack
Visited
graph_dfs.java
1import java.util.*;
2
3public class Main {
4 public static List<String> dfs(Map<String, List<String>> graph, String start) {
5 Deque<String> stack = new ArrayDeque<>();
6 Set<String> visited = new LinkedHashSet<>();
7 stack.push(start);
8 while (!stack.isEmpty()) {
9 String node = stack.pop();
10 if (!visited.add(node)) continue;
11 List<String> neighbors = graph.getOrDefault(node, List.of());
12 for (int i = neighbors.size() - 1; i >= 0; i--) stack.push(neighbors.get(i));
13 }
14 return new ArrayList<>(visited);
15 }
16
17}
● Java
Time
O(V + E)
Space
O(V)
Vertices
8
Edges
10
A: [B, C]
B: [A, D, E]
C: [A, E, F]
D: [B, G]
E: [B, C, G]
F: [C, H]
G: [D, E, H]
H: [F, G]
Stack (LIFO)1 frame
node Atop
Visit order
nothing visited yet

Written guide

Graph DFS: complete guide

Depth-first search follows one path as far as it can before backtracking, and the order in which it enters and leaves nodes reveals cycles, components and a topological order. Learn the recursive and iterative forms and when each one matters.

Intuition

Picture exploring a cave system with a ball of string. At every junction you pick an unexplored tunnel and keep going until you hit a dead end or a chamber you have already marked. Then you follow the string back to the last junction that still has an unexplored tunnel. That is depth-first search (DFS): commit to a direction, go deep, and backtrack only when forced.

What makes DFS powerful is not the order it visits nodes but the timing. Each node has a moment when DFS first enters it (pre-order) and a moment when DFS finishes all of its descendants and leaves it (post-order). Those two timestamps answer questions BFS cannot answer cheaply: does this directed graph contain a cycle, and in what order can its tasks be scheduled?

How it works step by step

  1. Give every node one of three colours: white (never seen), gray (entered, still on the current path) and black (fully finished).
  2. Call dfs(u) on a white node. Paint it gray and record it in the pre-order list.
  3. For each neighbor v of u: if v is white, recurse into it. If v is gray, you have found a back edge, which means a cycle in a directed graph. If v is black, skip it.
  4. When every neighbor is handled, paint u black and append it to the post-order list.
  5. Repeat from every node that is still white so disconnected parts are covered. Each fresh start in an undirected graph is a new connected component.
  6. For a DAG, reverse the post-order list to get a valid topological order.

Dry run on a small DAG

Directed edges 0→1, 0→2, 1→3, 2→3, 3→4. We start DFS at 0 and visit neighbors in list order. The stack column is the recursion stack after the action.

StepCall stackActionPost-order so far
1[0]Enter 0, paint gray (pre-order: 0)[]
2[0, 1]0→1 is white, enter 1 (pre: 0 1)[]
3[0, 1, 3]1→3 is white, enter 3 (pre: 0 1 3)[]
4[0, 1, 3, 4]3→4 is white, enter 4 (pre: 0 1 3 4)[]
5[0, 1, 3]4 has no edges, finish 4 and paint it black[4]
6[0, 1]3 is done, finish 3[4, 3]
7[0]1 is done, finish 1[4, 3, 1]
8[0, 2]0→2 is white, enter 2 (pre: 0 1 3 4 2)[4, 3, 1]
9[0]2→3 is black, skip; finish 2[4, 3, 1, 2]
10[]Finish 0[4, 3, 1, 2, 0]

Reversing the post-order gives 0, 2, 1, 3, 4, and every edge points forward in that list. If there were an edge 4→1, step 5 would find 1 still gray, which is a back edge and proof of the cycle 1→3→4→1.

Code

GraphDFS.java
import java.util.*;

public class GraphDFS {
    private static final int WHITE = 0, GRAY = 1, BLACK = 2;

    // Recursive DFS. Returns a topological order, or null if the graph has a cycle.
    public static List<Integer> topoSort(List<List<Integer>> adj) {
        int n = adj.size();
        int[] color = new int[n];
        List<Integer> post = new ArrayList<>();
        for (int u = 0; u < n; u++) {
            if (color[u] == WHITE && !visit(u, adj, color, post)) return null;
        }
        Collections.reverse(post);
        return post;
    }

    private static boolean visit(int u, List<List<Integer>> adj, int[] color, List<Integer> post) {
        color[u] = GRAY;
        for (int v : adj.get(u)) {
            if (color[v] == GRAY) return false;              // back edge: cycle
            if (color[v] == WHITE && !visit(v, adj, color, post)) return false;
        }
        color[u] = BLACK;
        post.add(u);                                         // post-order position
        return true;
    }

    // Undirected graph: count components with an explicit stack (no recursion).
    public static int countComponents(List<List<Integer>> adj) {
        boolean[] seen = new boolean[adj.size()];
        int components = 0;
        for (int s = 0; s < adj.size(); s++) {
            if (seen[s]) continue;
            components++;
            Deque<Integer> stack = new ArrayDeque<>();
            stack.push(s);
            seen[s] = true;
            while (!stack.isEmpty()) {
                int u = stack.pop();
                for (int v : adj.get(u)) {
                    if (!seen[v]) { seen[v] = true; stack.push(v); }
                }
            }
        }
        return components;
    }
}
graph_dfs.py
def topo_sort(n, edges):
    """Iterative three-colour DFS. Returns a topological order or None on a cycle."""
    adj = [[] for _ in range(n)]
    for u, v in edges:
        adj[u].append(v)

    WHITE, GRAY, BLACK = 0, 1, 2
    color = [WHITE] * n
    post = []
    for start in range(n):
        if color[start] != WHITE:
            continue
        color[start] = GRAY
        stack = [(start, iter(adj[start]))]      # node plus where we are in its list
        while stack:
            node, neighbors = stack[-1]
            nxt = next(neighbors, None)
            if nxt is None:                      # all children done: post-order
                color[node] = BLACK
                post.append(node)
                stack.pop()
            elif color[nxt] == GRAY:
                return None                      # back edge
            elif color[nxt] == WHITE:
                color[nxt] = GRAY
                stack.append((nxt, iter(adj[nxt])))
    return post[::-1]


print(topo_sort(5, [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]))  # [0, 2, 1, 3, 4]
print(topo_sort(3, [(0, 1), (1, 2), (2, 0)]))                  # None

The Python version keeps an iterator per stack frame, which is how you get true post-order without recursion. The simpler push-all-neighbors stack used in countComponents is fine for reachability but does not give correct finish times.

Time and space complexity

MeasureCostWhy
TimeO(V + E)Every node changes colour twice and every adjacency list is walked once
Space (colours, output)O(V)One colour per node plus the post-order list
Stack depthO(V) worst caseA path graph puts every node on the stack at once, recursive or explicit

Edge cases

  • Self-loop u→u: u is gray when it sees itself, so it is reported as a cycle, which is correct for directed graphs.
  • Undirected cycle detection: every edge is seen from both ends, so ignore the edge back to the parent and only flag other visited neighbors.
  • Several valid topological orders: interviewers usually accept any; do not hard-code one expected answer in your tests.
  • Empty graph or isolated nodes: each isolated node is its own component and can appear anywhere in a topological order.
  • Deep chains: Java's default thread stack and Python's recursion limit of about 1000 can overflow on a long path; switch to the iterative form.

Common mistakes

  • Using a single boolean visited array for directed cycle detection. A black node reached through a different branch is not a cycle; only a gray node is.
  • Recording the node in pre-order and reversing that for a topological sort. Only reversed post-order is guaranteed to be valid.
  • Forgetting the outer loop over all nodes, which silently skips disconnected parts of the graph.
  • Assuming the explicit-stack version visits nodes in the same order as recursion. Pushing neighbors reverses their order unless you push them backwards.
  • Treating DFS distance as shortest distance. DFS finds a path, not the shortest one.

Variations and when to use it

Pick DFS when the question is about structure rather than distance: is everything reachable, how many groups are there, is there a cycle, which order respects these dependencies. The same skeleton powers Tarjan's bridges and articulation points (using discovery times and low-links), strongly connected components (Kosaraju runs DFS twice), and backtracking searches that enumerate every path. If you prefer a queue-based topological sort that also detects cycles by counting processed nodes, compare it with Kahn's algorithm. When the graph is a 2D grid rather than an adjacency list, see DFS on a grid.

  • #547 Number of Provinces — count how many times the outer loop starts a fresh DFS.
  • #841 Keys and Rooms — DFS from room 0 and check that every room was visited.
  • #1466 Reorder Routes to Make All Paths Lead to the City Zero — DFS from city 0 over undirected edges, counting the ones that point away.
  • #207 Course Schedule — three-colour cycle detection on the prerequisite graph.
  • #210 Course Schedule II — the same DFS, returning reversed post-order.
  • #802 Find Eventual Safe States — nodes that finish black without touching a gray node are safe.
  • #1192 Critical Connections in a Network — DFS with discovery times and low-links finds bridges.

FAQ

›What is the difference between pre-order and post-order in graph DFS?

Pre-order records a node when DFS first enters it; post-order records it after all of its descendants are finished. Reversed post-order of a DAG is a topological order.

›How does DFS detect a cycle in a directed graph?

Keep three states. Reaching a node that is still gray, meaning it is on the current recursion path, is a back edge and proves a cycle.

›Should I write DFS recursively or with a stack?

Recursion is shorter and natural for post-order logic. Use an explicit stack when the graph can be deep enough to overflow the call stack, which is common in Python.

›Can DFS find the shortest path?

Not in general. It returns the first path it stumbles on. Use BFS for unweighted shortest paths and Dijkstra for weighted ones.

›How do I count connected components?

Loop over every node, and each time you find an unvisited one, increment a counter and run DFS from it to mark its whole component.