Hello! Welcome to the second lesson in our module on Graph Traversal Algorithms.
In our previous lesson, we established the groundwork by learning how to represent graphs in code, focusing on the adjacency list as the most efficient method for the sparse graphs common in competitive programming. You learned to build these lists from a set of edges using Python's collections.defaultdict.
Today, we build directly on that foundation. Now that we can store a graph, we need a way to systematically visit its vertices. We will implement our first major graph traversal algorithm: Depth-First Search (DFS). The goal of this lesson is to understand the "go-deep-first" philosophy of DFS and to implement it in Python using two distinct but related approaches: a simple, elegant recursive method and a robust iterative method using a stack.
What is Depth-First Search?
Depth-First Search is an algorithm for traversing a graph. As the name suggests, it prioritizes depth. Imagine you're exploring a maze. DFS is like choosing one path and following it as far as you can go. When you hit a dead end, you backtrack to the last junction where you had an unexplored path, and you dive down that new path. You repeat this until all paths from your starting point have been explored.
This "dive deep, then backtrack" strategy is a powerful tool for many problems, including finding paths, detecting cycles, and exploring all possible solutions in a search space.
To get a feel for this process, take a look at the following image which illustrates a step-by-step DFS traversal on a tree.

A crucial element in any graph traversal is keeping track of where you've already been. Without this, if the graph contains a cycle (e.g., A -> B -> C -> A), the algorithm would loop forever. To prevent this, we use a set (often called visited or seen) to store the nodes we have already processed.
For a more dynamic and intuitive explanation of this core idea, the following video is highly recommended.
Depth First Search (DFS) Explained: Algorithm, Examples, and Code
The video 'Depth First Search (DFS) Explained' from Reducible provides an excellent intuitive walkthrough of the DFS algorithm, showing how the logic develops naturally.
Watch from 00:46 to 06:21. Focus on the thought process: how do we explore from a vertex, what do we do when we see a vertex we've already visited, and what does it mean to 'backtrack'?
Recursive Implementation of DFS
The "dive deep and backtrack" nature of DFS maps perfectly to recursion. The act of "diving deep" is simply a recursive call on a neighbor. When that call finishes, the function returns, which is the "backtracking" step. The program's own call stack implicitly manages the path for us.
The logic is straightforward:
- Start at a given
node. Mark it as visited. - Process the node (e.g., print its value).
- For each
neighborof the currentnode: - If the
neighborhas not been visited, recursively call the DFS function on it.
Let's see this translated into Python code. The following video implements this logic using the defaultdict adjacency list we created in the last lesson.
Graphs: Edge List, Adjacency Matrix, Adjacency List, DFS, BFS - DSA Course in Python Lecture 11
This segment from Greg Hogg's DSA course shows a concise, practical implementation of recursive DFS, ideal for a competitive programming context.
Watch from 26:29 to 28:03. Observe how a helper function is used for the recursion and how the seen set prevents visiting the same node twice.
Here is the code, which you should be able to follow along with the video:
from collections import defaultdict
# Assume 'adj' is our adjacency list (a defaultdict)
# adj = defaultdict(list)
# ... edges are added here ...
# Set to keep track of visited nodes
seen = set()
def dfs_recursive(graph, node):
# Process the node (e.g., print it)
print(node, end=" ")
# Mark the current node as seen
seen.add(node)
# Recurse for all adjacent vertices that haven't been seen
for neighbor in graph[node]:
if neighbor not in seen:
dfs_recursive(graph, neighbor)
# --- Example Usage ---
# Build the graph from the video
edges = [[0, 1], [0, 3], [1, 2], [3, 4], [3, 6], [3, 7], [4, 5], [4, 2], [5, 2], [5, 4]]
n = 8
adj = defaultdict(list)
for u, v in edges:
adj[u].append(v)
print("Recursive DFS traversal:")
# To start the traversal, call the function with the starting node
start_node = 0
dfs_recursive(adj, start_node)
print("\n")
The recursive approach is often preferred for its simplicity and readability when the graph isn't excessively deep.
Iterative Implementation of DFS
While recursion is elegant, it has a limitation: Python has a recursion depth limit. For very deep graphs (like a long linked list), a recursive DFS could cause a RecursionError. The solution is an iterative implementation using an explicit stack.
The logic is as follows:
- Create a
stackand push thestart_nodeonto it. - Create a
seenset to track visited nodes. - While the
stackis not empty: - Pop a
nodefrom the stack. - If the
nodehas already been seen,continue. - Mark the
nodeas seen and process it. - Push all unseen
neighborsof thenodeonto the stack.
The following video clip demonstrates this iterative approach.
Graphs: Edge List, Adjacency Matrix, Adjacency List, DFS, BFS - DSA Course in Python Lecture 11
Continuing with Greg Hogg's video, this part explains how to implement DFS iteratively using a list as a stack.
Watch from 28:03 to 29:21. Note the similarities and differences compared to the recursive version. The core loop pops from a stack and adds neighbors to it.
Here is the corresponding Python code:
def dfs_iterative(graph, start_node):
seen = set()
stack = [start_node] # Start with the initial node
while stack:
node = stack.pop() # Get the last node added
if node not in seen:
# Process the node
print(node, end=" ")
seen.add(node)
# Add all unseen neighbors to the stack.
# Note: The order of traversal can depend on the order neighbors are added.
# To match the typical recursive order, you might add them in reverse.
for neighbor in reversed(graph[node]): # reversed() is optional but can help consistency
if neighbor not in seen:
stack.append(neighbor)
# --- Example Usage ---
# Using the same adjacency list 'adj' from before
print("Iterative DFS traversal:")
dfs_iterative(adj, start_node=0)
print("\n")
Notice the traversal order might differ from the recursive one. This is because the recursive version explores the first neighbor deeply, while the iterative version adds all neighbors to the stack and then explores the last one added. This difference is generally fine unless a specific traversal order is required by a problem.
Test your understanding!
Consider the following graph represented by an adjacency list:
graph = {
0: [1, 2],
1: [0, 3],
2: [0, 3],
3: [1, 2, 4],
4: [3]
}
Starting from node 0, what is one possible traversal order for:
- A recursive DFS? (Assume neighbors are explored in the order they appear in the list).
- An iterative DFS? (Assume neighbors are pushed to the stack in the order they appear, i.e.,
reversedis not used).
Show answer
-
Recursive DFS:
- Start at
0. Print0. - Go to neighbor
1. Print1. 1's neighbors are0(seen) and3. Go to3. Print3.3's neighbors are1(seen),2, and4. Go to2. Print2.2's neighbors are0(seen) and3(seen). Backtrack to3.3's next unseen neighbor is4. Go to4. Print4.4's only neighbor3is seen. Backtrack to3,1, then0.0's next neighbor2is seen. Traversal ends.- Possible order:
0 1 3 2 4
- Start at
-
Iterative DFS:
stack = [0]- Pop
0. Print0. Push neighbors2, then1.stack = [2, 1] - Pop
1. Print1. Push neighbor3.stack = [2, 3] - Pop
3. Print3. Push neighbors4,2(seen),1(seen).stack = [2, 4] - Pop
4. Print4. Neighbor3is seen.stack = [2] - Pop
2. Print2. Neighbor0and3are seen.stack = [] - Traversal ends.
- Possible order:
0 1 3 4 2
Your own trace might differ slightly depending on the neighbor order, which is perfectly fine! The key is the depth-first exploration pattern.
Complexity and When to Use Which
For a graph with vertices and edges, both implementations of DFS have the same complexity:
- Time Complexity: . This is because in the worst case, DFS visits every vertex and traverses every edge exactly once.
- Space Complexity: . In the recursive version, this space is used by the call stack (which can be as deep as the number of vertices in the worst case, e.g., a path graph). In the iterative version, this is the space used by the
stackand theseenset.
Recursive vs. Iterative:
- Recursive: Simpler to write and read. Excellent for most competitive programming scenarios.
- Iterative: Use this if you anticipate a very deep graph that might cause a stack overflow, or if you need finer control over the stack's contents.
Conclusion
You have now mastered Depth-First Search, a cornerstone of graph algorithms. You understand its core principle and can confidently implement it both recursively and iteratively.
Key Takeaways:
- DFS explores a graph by going as deep as possible down one path before backtracking.
- A
seenset is essential to track visited nodes and prevent infinite loops in graphs with cycles. - The recursive implementation is elegant and uses the system's call stack to manage backtracking. Its time complexity is .
- The iterative implementation uses an explicit stack (e.g., a Python list) to avoid recursion limits, making it more robust for very deep graphs. It shares the same time complexity.
Up Next:
In the next lesson, we will explore the other fundamental graph traversal algorithm: Breadth-First Search (BFS). You'll see how a simple switch from a stack to a queue completely changes the traversal strategy from "depth-first" to "level-by-level", and you'll learn which types of problems are best suited for each.