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.

1. The Core Idea: Diving Deep Before Going Wide
The principle behind DFS is simple yet powerful:
- Start at a given source node.
- Pick one of its neighbors and immediately move to it.
- From that new node, pick one of its neighbors and move again.
- Continue this process, going deeper and deeper into the graph, until you reach a node with no unvisited neighbors (a "dead end").
- 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:
- Define a helper function, say
dfsUtil(node, visited, adj, result). - In this function, first, mark the current
nodeas visited. - Add the
nodeto your traversal result list. - Iterate through all the neighbors of the current
node. - 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:
- Create a
Stackand push the starting node onto it. - Create a
visitedarray/set. - Loop as long as the stack is not empty:
a. Pop a nodeufrom the stack.
b. Ifuhas already been visited,continueto the next iteration.
c. Markuas visited and add it to the result.
d. Iterate through the neighbors ofuand 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)
Vis the number of vertices, andEis 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*Efor an undirected graph (orEfor a directed one), we examine each edge. This accounts for theO(E)part.
-
Space Complexity: O(V)
- The
visitedarray/set requiresO(V)space. - The recursion call stack (for recursive DFS) or the explicit
Stack(for iterative DFS) in the worst case can hold up toVvertices. This happens in a graph that is a single long chain. So, the auxiliary space isO(V).
- The
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.