Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 72

Implementing DFS: Recursive & Iterative Approaches

Recommended resources

Hello! Welcome back to our journey through graph algorithms.

In our last lesson, we explored Breadth-First Search (BFS), a technique that traverses a graph layer by layer, much like ripples in a pond. We saw how its use of a queue makes it perfect for finding the shortest path in an unweighted graph.

Today, we shift our focus to the other fundamental traversal algorithm: Depth-First Search (DFS). This lesson directly addresses the learning outcome: Implement Depth-First Search (DFS) on a graph, both recursively and iteratively. Instead of exploring widely like BFS, DFS dives deep, exploring as far as possible along one path before backtracking. This "maze-solving" approach opens the door to a different set of powerful applications.

Graph Traversal: DFS vs. BFS Example
A visual comparison of DFS and BFS on the same graph starting from node 0. While BFS explores neighbors `1` and `2` first, DFS dives down the path `0 -> 1 -> 3` before exploring other branches. The DFS path shown is one of several possibilities.

1. The Core Idea: Diving Deep Before Going Wide

The principle behind DFS is simple yet powerful:

  1. Start at a given source node.
  2. Pick one of its neighbors and immediately move to it.
  3. From that new node, pick one of its neighbors and move again.
  4. Continue this process, going deeper and deeper into the graph, until you reach a node with no unvisited neighbors (a "dead end").
  5. At the dead end, you backtrack to the previous node and try its next unvisited neighbor.

This process naturally lends itself to two implementation styles: recursion, where the call stack handles backtracking for us, and iteration, where we use an explicit Stack to manage the nodes to visit.

To build a strong mental model of this process, let's watch a short conceptual overview.

G-6. Depth-First Search (DFS) | C++ and Java | Traversal Technique in Graphs

This clip from the 'take U forward' channel provides an excellent intuition for the DFS traversal process. It visually demonstrates how the algorithm chooses a path and follows it to its end before backtracking.

Watch from the beginning (00:03) to 04:44. Focus on the concept of going to the 'depth' of a path and how the traversal order is formed.


2. Recursive DFS: The Natural Approach

Given your background with programming and recursion, you'll see that it's a very elegant way to express DFS. The function call stack implicitly keeps track of the path we've taken, making backtracking automatic when a function call returns.

The algorithm for a recursive DFS is straightforward:

  1. Define a helper function, say dfsUtil(node, visited, adj, result).
  2. In this function, first, mark the current node as visited.
  3. Add the node to your traversal result list.
  4. Iterate through all the neighbors of the current node.
  5. If a neighbor has not been visited, make a recursive call: dfsUtil(neighbor, ...).

Let's see this translated into Java.

🔍 Understanding Depth-First Search (DFS) with Java Code ...

The article 'Understanding Depth-First Search (DFS) with Java Code Examples' provides a clean, object-oriented implementation of recursive DFS. We'll focus on its core logic.

In the webpage, find the section titled '1. DFS Recursive Implementation (for Trees and Graphs)'. Study the dfsRecursive method. Notice how it marks a node as visited and then iterates through its neighbors, calling itself on any that are unvisited.

Complete Recursive Implementation

Here is a full, runnable example. Note the main dfs method that handles initialization and the case of disconnected graphs by looping through all vertices. This is a crucial detail for interview problems, as you can't assume a graph is a single connected component.

import java.util.ArrayList;
import java.util.List;

public class DFSRecursive {

    // Main DFS function that handles disconnected components
    public static List<Integer> dfsTraversal(int V, ArrayList<ArrayList<Integer>> adj) {
        boolean[] visited = new boolean[V];
        List<Integer> result = new ArrayList<>();

        // Loop through all vertices in case the graph is disconnected
        for (int i = 0; i < V; i++) {
            if (!visited[i]) {
                dfsUtil(i, visited, adj, result);
            }
        }
        return result;
    }

    // Recursive helper function
    private static void dfsUtil(int u, boolean[] visited, ArrayList<ArrayList<Integer>> adj, List<Integer> result) {
        // Mark the current node as visited and add it to the result
        visited[u] = true;
        result.add(u);

        // Recur for all the vertices adjacent to this vertex
        for (int v : adj.get(u)) {
            if (!visited[v]) {
                dfsUtil(v, visited, adj, result);
            }
        }
    }

    public static void main(String[] args) {
        int V = 5;
        ArrayList<ArrayList<Integer>> adj = new ArrayList<>(V);
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }

        // Example from the image at the start
        adj.get(0).add(1);
        adj.get(1).add(0);

        adj.get(0).add(2);
        adj.get(2).add(0);

        adj.get(1).add(3);
        adj.get(3).add(1);
        
        adj.get(2).add(4);
        adj.get(4).add(2);

        System.out.println("DFS Traversal (Recursive):");
        List<Integer> traversal = dfsTraversal(V, adj);
        System.out.println(traversal); // Example Output: [0, 1, 3, 2, 4]
    }
}

3. Iterative DFS: Using an Explicit Stack

While recursion is elegant, it can lead to a StackOverflowError if the graph is very deep. An iterative approach using an explicit Stack data structure avoids this and gives you more control. It perfectly mimics what the recursive call stack does behind the scenes.

The algorithm for iterative DFS is:

  1. Create a Stack and push the starting node onto it.
  2. Create a visited array/set.
  3. Loop as long as the stack is not empty:
    a. Pop a node u from the stack.
    b. If u has already been visited, continue to the next iteration.
    c. Mark u as visited and add it to the result.
    d. Iterate through the neighbors of u and push all unvisited neighbors onto the stack.

This process is demonstrated very clearly in the following video.

Graph Data Structure | Part 5 | Depth First Search and Traversal | DFS

To implement DFS iteratively, we replace the call stack with a java.util.Stack. This video from Nikhil Lohia gives a clear, animated walkthrough of how the stack manages the traversal order.

Watch from 03:59 to 13:37. This covers both the step-by-step logic with a sample graph and the corresponding code structure. Focus on how popping from the stack and pushing neighbors drives the algorithm.

Complete Iterative Implementation

Here is the full Java code for an iterative DFS. This example uses a HashSet for the visited tracker, which is also a common and effective approach.

import java.util.*;

public class DFSIterative {

    public static List<Integer> dfsTraversal(int V, ArrayList<ArrayList<Integer>> adj, int startNode) {
        List<Integer> result = new ArrayList<>();
        Set<Integer> visited = new HashSet<>();
        Stack<Integer> stack = new Stack<>();

        stack.push(startNode);

        while (!stack.isEmpty()) {
            int u = stack.pop();

            // If we've already visited this node, skip it.
            // This is important because a node can be pushed to the stack multiple times
            // by different parents before it gets processed.
            if (visited.contains(u)) {
                continue;
            }

            visited.add(u);
            result.add(u);

            // Get all adjacent vertices of the popped vertex u.
            // Push them to the stack if they haven't been visited.
            // Note: Pushing in this order means we'll visit neighbors in reverse
            // order of the adjacency list. For traversal, this is fine.
            ArrayList<Integer> neighbors = adj.get(u);
            for (int i = neighbors.size() - 1; i >= 0; i--) {
                int v = neighbors.get(i);
                if (!visited.contains(v)) {
                    stack.push(v);
                }
            }
        }
        return result;
    }
    
    public static void main(String[] args) {
        int V = 5;
        ArrayList<ArrayList<Integer>> adj = new ArrayList<>(V);
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }

        adj.get(0).add(1);
        adj.get(1).add(0);

        adj.get(0).add(2);
        adj.get(2).add(0);

        adj.get(1).add(3);
        adj.get(3).add(1);
        
        adj.get(2).add(4);
        adj.get(4).add(2);
        
        System.out.println("DFS Traversal (Iterative):");
        // Note: For a disconnected graph, you'd need an outer loop like the recursive version.
        List<Integer> traversal = dfsTraversal(V, adj, 0);
        System.out.println(traversal); // Example Output: [0, 1, 3, 2, 4]
    }
}
Test your understanding!

Consider the graph from our code example: 0 is connected to 1 and 2, 1 to 3, 2 to 4. If the adjacency list for node 0 was [2, 1] instead of [1, 2], what would the recursive DFS traversal path be, starting from 0?

Show answer

The traversal would be [0, 2, 4, 1, 3]. The algorithm would first explore the path starting with neighbor 2 to its depth (0 -> 2 -> 4), then backtrack and explore the path starting with neighbor 1 (1 -> 3). This highlights that the DFS path is dependent on the order of neighbors in the adjacency list.


4. Complexity Analysis

For both the recursive and iterative versions, the complexity is the same and is a crucial point for interviews.

  • Time Complexity: O(V + E)

    • V is the number of vertices, and E is the number of edges.
    • Each vertex is visited (pushed/popped or entered in a recursive call) exactly once. This accounts for the O(V) part.
    • We iterate through the adjacency list of every vertex. Since the sum of the lengths of all adjacency lists is 2*E for an undirected graph (or E for a directed one), we examine each edge. This accounts for the O(E) part.
  • Space Complexity: O(V)

    • The visited array/set requires O(V) space.
    • The recursion call stack (for recursive DFS) or the explicit Stack (for iterative DFS) in the worst case can hold up to V vertices. This happens in a graph that is a single long chain. So, the auxiliary space is O(V).

Conclusion

You have now implemented the two most fundamental graph traversal algorithms: BFS and DFS. Understanding their differences is key to knowing when to apply them.

Key Takeaways:

  • DFS explores a graph by going as deep as possible down a path before backtracking.
  • It can be implemented recursively, where the call stack manages backtracking, or iteratively, using an explicit Stack.
  • The time complexity is O(V + E), and space complexity is O(V), similar to BFS.
  • The specific traversal path depends on the order of neighbors in the adjacency list.
  • Handling disconnected graphs requires an outer loop to ensure all vertices are visited.

Preview of the Next Lesson:
Now that we have mastered BFS and DFS traversals, we can start applying them to solve classic graph problems. In our next lesson, we will use these new skills to detect a cycle in an undirected graph. You'll see how the properties of both traversal methods can be cleverly used to identify back edges, which are the tell-tale sign of a cycle.