← All Posts
DSA Series · Graphs · Traversal · DFS

DFS: Depth-First Search In Depth

DFS dives as deep as possible into the graph before backtracking. Where BFS explores level by level (breadth), DFS explores branch by branch (depth). This behavior makes DFS the natural choice for problems involving paths, cycles, connected components, and anything that benefits from exploring one direction fully before trying another.

The Core Idea

Think of exploring a maze. At every junction, you pick one unexplored path and follow it. When you hit a dead end, you backtrack to the last junction and try the next path. That is DFS.

DFS naturally uses a stack. Recursion provides an implicit stack (the call stack), which is why recursive DFS is the most common form. You can also write it iteratively with an explicit stack.

0 1 2 3 4 5 DFS dives deep first explores later
DFS from node 0: dives to 3, then 5 (as deep as possible) before backtracking to explore 1, 2, 4.

The Algorithm

Recursive DFS

void dfs(int node, vector<vector<int>>& adj, vector<bool>& visited) {
    visited[node] = true;
    // Process node here (pre-order)

    for (int neighbor : adj[node]) {
        if (!visited[neighbor]) {
            dfs(neighbor, adj, visited);
        }
    }
    // Post-processing here (post-order)
}

The recursion handles the stack implicitly. Each recursive call pushes a frame. When it returns, it backtracks (pops the frame).

Iterative DFS

void dfs_iterative(int source, vector<vector<int>>& adj) {
    int n = adj.size();
    vector<bool> visited(n, false);
    stack<int> st;

    st.push(source);

    while (!st.empty()) {
        int node = st.top();
        st.pop();

        if (visited[node]) continue;
        visited[node] = true;
        // Process node

        for (int neighbor : adj[node]) {
            if (!visited[neighbor]) {
                st.push(neighbor);
            }
        }
    }
}
Note: iterative DFS visits nodes in a different order than recursive DFS (because the stack reverses the neighbor order). If order matters, iterate neighbors in reverse when pushing. For most problems, either order works.

Visual Step-by-Step Walkthrough

Tracing recursive DFS on the same graph, starting from node 0. Neighbors are processed in ascending order.

0 1 2 3 4 5
Graph: 0-1, 0-3, 1-2, 1-4, 3-4, 3-5. Adjacency lists sorted ascending.

dfs(0): mark 0 visited, first neighbor is 1

Enter node 0. Mark visited. Neighbors are [1, 3]. First unvisited neighbor is 1. Recurse into dfs(1).

0 1 2 3 4 5
Call stack: [dfs(0), dfs(1)]

dfs(1): mark 1 visited, first unvisited neighbor is 2

Enter node 1. Neighbors are [0, 2, 4]. Node 0 is visited. First unvisited is 2. Recurse into dfs(2).

0 1 2 3 4 5
Call stack: [dfs(0), dfs(1), dfs(2)]

dfs(2): mark 2 visited, neighbors 1 and 4. 1 is visited, recurse into 4

Node 2's neighbors: [1, 4]. Node 1 is visited. Recurse into dfs(4).

0 1 2 3 4 5
Call stack: [dfs(0), dfs(1), dfs(2), dfs(4)]

dfs(4): mark 4 visited, neighbors 1, 2, 3. Only 3 is unvisited. Recurse into 3.

Node 4's neighbors: [1, 2, 3]. Nodes 1 and 2 are visited. Recurse into dfs(3).

0 1 2 3 4 5
Call stack: [dfs(0), dfs(1), dfs(2), dfs(4), dfs(3)]

dfs(3): mark 3 visited, neighbors 0, 4, 5. Only 5 is unvisited. Recurse into 5.

Node 3's neighbors: [0, 4, 5]. Nodes 0 and 4 visited. Recurse into dfs(5).

0 1 2 3 4 5
Call stack: [dfs(0), dfs(1), dfs(2), dfs(4), dfs(3), dfs(5)]

This is the deepest point. The stack is at maximum depth (6 frames).

dfs(5): mark 5 visited, neighbor 3 is visited. Dead end. Backtrack.

Node 5's only neighbor is 3 (visited). No unvisited neighbors. Return from dfs(5). Then return from dfs(3), dfs(4), dfs(2), dfs(1), dfs(0). All calls unwind.

0 1 2 3 4 5
Call stack: [] (all returned)
DFS complete. Visit order: 0, 1, 2, 4, 3, 5

Edge Classification

During DFS on a directed graph, every edge falls into one of four categories. This classification is essential for cycle detection and topological sort.

Tree edge Back edge Forward edge Cross edge

Tree Edges

Edges that DFS actually traverses to discover new nodes. They form the DFS tree.

Back Edges

Edge from a node to an ancestor in the DFS tree. Back edges indicate cycles. A directed graph has a cycle if and only if DFS finds a back edge.

Forward Edges

Edge from a node to a descendant in the DFS tree (but not a tree edge). Rare in practice.

Cross Edges

Edge between nodes where neither is an ancestor of the other. Goes between separate branches.

To classify edges, use a coloring scheme:

enum Color { WHITE, GRAY, BLACK };
vector<Color> color(n, WHITE);

void dfs(int u) {
    color[u] = GRAY;  // entering
    for (int v : adj[u]) {
        if (color[v] == WHITE) {
            // Tree edge: u -> v
            dfs(v);
        } else if (color[v] == GRAY) {
            // Back edge: u -> v (CYCLE!)
        } else {
            // Forward or cross edge
        }
    }
    color[u] = BLACK;  // exiting
}

Pre-order and Post-order in DFS

DFS visits each node twice: once when entering (discovery) and once when exiting (finishing). This gives two orderings:

int timer = 0;
vector<int> disc(n), fin(n);

void dfs(int u) {
    disc[u] = timer++;  // pre-order timestamp
    visited[u] = true;
    for (int v : adj[u]) {
        if (!visited[v]) dfs(v);
    }
    fin[u] = timer++;  // post-order timestamp
}

For our graph (DFS from 0, neighbors ascending):

Node Discovery Finish
0011
1110
229
438
347
556

Interactive Animation

Step through recursive DFS from node 0. Red outline = in recursion stack. Green = fully processed.

0 1 2 3 4 5

Common DFS Patterns

Connected Components

int components = 0;
vector<bool> visited(n, false);
for (int i = 0; i < n; i++) {
    if (!visited[i]) {
        dfs(i, adj, visited);
        components++;
    }
}

Cycle Detection (Directed Graph)

Use the gray/black coloring from above. A back edge (edge to a gray node) means a cycle exists.

Flood Fill

DFS on a grid. Start at a cell, visit all connected cells of the same color. Classic "number of islands" problem.

void floodFill(vector<vector<int>>& grid, int r, int c, int color) {
    if (r < 0 || r >= m || c < 0 || c >= n) return;
    if (grid[r][c] != oldColor) return;
    grid[r][c] = color;
    floodFill(grid, r+1, c, color);
    floodFill(grid, r-1, c, color);
    floodFill(grid, r, c+1, color);
    floodFill(grid, r, c-1, color);
}

Complexity

Stack overflow warning: for very deep graphs (V > 105), recursive DFS may overflow the call stack. Use iterative DFS in those cases, or increase the stack size.

Common Mistakes