Skip to main content

Welcome to a Grasp lesson

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

Lesson 56

DFS: Recursive & Iterative Implementations

Recommended resources

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.

Step-by-Step Depth-First Search (DFS) Traversal on a Tree
This image shows how DFS explores. Notice how it goes from 1 to 2 to 4 to 8 (a deep path) before backtracking to 4 and exploring its other child, 9.

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:

  1. Start at a given node. Mark it as visited.
  2. Process the node (e.g., print its value).
  3. For each neighbor of the current node:
  4. If the neighbor has 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:

  1. Create a stack and push the start_node onto it.
  2. Create a seen set to track visited nodes.
  3. While the stack is not empty:
  4. Pop a node from the stack.
  5. If the node has already been seen, continue.
  6. Mark the node as seen and process it.
  7. Push all unseen neighbors of the node onto 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:

  1. A recursive DFS? (Assume neighbors are explored in the order they appear in the list).
  2. An iterative DFS? (Assume neighbors are pushed to the stack in the order they appear, i.e., reversed is not used).
Show answer
  1. Recursive DFS:

    • Start at 0. Print 0.
    • Go to neighbor 1. Print 1.
    • 1's neighbors are 0 (seen) and 3. Go to 3. Print 3.
    • 3's neighbors are 1 (seen), 2, and 4. Go to 2. Print 2.
    • 2's neighbors are 0 (seen) and 3 (seen). Backtrack to 3.
    • 3's next unseen neighbor is 4. Go to 4. Print 4.
    • 4's only neighbor 3 is seen. Backtrack to 3, 1, then 0.
    • 0's next neighbor 2 is seen. Traversal ends.
    • Possible order: 0 1 3 2 4
  2. Iterative DFS:

    • stack = [0]
    • Pop 0. Print 0. Push neighbors 2, then 1. stack = [2, 1]
    • Pop 1. Print 1. Push neighbor 3. stack = [2, 3]
    • Pop 3. Print 3. Push neighbors 4, 2 (seen), 1 (seen). stack = [2, 4]
    • Pop 4. Print 4. Neighbor 3 is seen. stack = [2]
    • Pop 2. Print 2. Neighbor 0 and 3 are 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 stack and the seen set.

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 seen set 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.