Skip to main content

Welcome to a Grasp lesson

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

Lesson 54

Exploring Graphs with DFS

Recommended resources

Hello! Welcome back to our series on graph algorithms.

In the last lesson, we mastered Breadth-First Search (BFS), discovering how its level-by-level exploration using a queue is perfect for finding the shortest path in an unweighted graph.

Today, we switch gears from breadth to depth. We will implement Depth-First Search (DFS), a powerful traversal algorithm that explores as far as possible along each branch before backtracking. This approach, driven by a stack, unlocks solutions to a different, but equally important, set of interview problems.

Our learning outcome is to implement Depth-First Search (DFS) for graph exploration and path finding. We'll cover both the elegant recursive and the robust iterative implementations, and see how to adapt them to find if a path exists and to reconstruct that path.

1. The Core Idea: Going Deep

While BFS uses a queue to explore layer by layer (like ripples in a pond), DFS uses a stack to go deep. Imagine you're in a maze. Instead of checking every path of length 1, then every path of length 2, you'd pick one path and follow it until you hit a dead end. Only then would you backtrack to your last turn and try a different route. This is the essence of DFS.

To implement this "last-in, first-out" exploration strategy, DFS relies on a stack. This can be the program's own function call stack (in a recursive implementation) or an explicit stack data structure (in an iterative one).

To get a clear visual of how DFS works and differs from BFS, and to understand the critical role of a visited set, please watch the following video segment.

Graphs: Edge List, Adjacency Matrix, Adjacency List, DFS, BFS - DSA Course in Python Lecture 11

This video from Greg Hogg provides an excellent visualization of DFS. It walks through the traversal, explains why a visited set is essential to avoid infinite loops in graphs with cycles, and then contrasts the recursive and iterative approaches.

Please watch from 07:39 to 14:26. The first part (until 11:22) visualizes recursive DFS, and the second part demonstrates iterative DFS with an explicit stack. Pay close attention to how the stack's LIFO (Last-In, First-Out) nature drives the deep exploration.


2. Recursive DFS: The Intuitive Approach

For many developers, the recursive implementation of DFS is the most natural. The function call stack elegantly handles the backtracking for us.

The logic is straightforward:

  1. From a given node, mark it as visited.
  2. Then, for each of its unvisited neighbors, recursively call the function on that neighbor.
  3. The recursion naturally "backtracks" when a path is fully explored (i.e., it hits a node with no unvisited neighbors).
DFS Implementation in Python with Graph Visualization
CAPTION: This image shows a directed graph, its adjacency list, and a clean, recursive Python implementation of DFS. Notice how the `dfs` function calls itself for each `neighbour` that has not been visited, naturally creating the deep traversal path.

Here is a standard template for a recursive DFS. This structure is a great starting point for many interview problems.

# graph is an adjacency list, e.g., {'A': ['B', 'C'], ...}
# node is the starting node
# visited is a set to keep track of visited nodes

def dfs_recursive(graph, node, visited):
    if node in visited:
        return

    visited.add(node)
    print(f"Visiting {node}") # Process the node

    for neighbor in graph.get(node, []):
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited)

# --- Driver Code ---
# graph = ...
# visited = set()
# dfs_recursive(graph, 'A', visited)

This simple traversal is the foundation. Now, let's see the iterative version.


3. Iterative DFS: The Explicit Stack

While recursion is elegant, it can lead to a "maximum recursion depth exceeded" error on very large or deep graphs. An iterative approach using an explicit stack avoids this and gives you more control.

The logic mirrors the stack-based visualization you saw in the video:

  1. Initialize a stack and push the starting node onto it.
  2. Initialize a visited set.
  3. While the stack is not empty, pop a node.
  4. If the node has already been visited, skip it.
  5. Mark the node as visited and process it.
  6. Push all its unvisited neighbors onto the stack.
DFS Graph Traversal - Step 2
CAPTION: This image captures a moment during an iterative DFS. Node 0 was visited, then its neighbor 1. Now, nodes 2 and 3 (neighbors of 0 and 1) are on the stack. The next node to be processed will be popped from the top of the stack (node 2), continuing the deep dive.

This code snippet from the "Master Graph Algorithms" article provides a clean iterative implementation.

Master Graph Algorithms for Coding Interviews

The article from AlgoMaster provides concise and clear Python implementations for both recursive and iterative DFS. We'll focus on the iterative version here.

Scroll down to the 'Implementation' section within the '1. Depth First Search (DFS)' part of the article. Review the code block for dfs_iterative.

Test your understanding!

Consider the iterative DFS. The order in which neighbors are pushed onto the stack can change the traversal path. If graph = {'A': ['B', 'C']} and you are at node 'A', pushing 'C' then 'B' means 'B' will be on top and visited next.

Given the graph graph = {0: [1, 2], 1: [3], 2: [4]} and starting at node 0, what are two possible DFS traversal orders? One where you explore node 1's branch first, and one where you explore node 2's branch first.

Show answer
  1. Exploring 1's branch first: 0, 1, 3, 2, 4. (Neighbors of 0 are pushed as 2, then 1. 1 is popped first).
  2. Exploring 2's branch first: 0, 2, 4, 1, 3. (Neighbors of 0 are pushed as 1, then 2. 2 is popped first).

Both are valid DFS traversals. The specific path depends on the order of neighbors in the adjacency list and the implementation details.


4. Application: Path Finding with DFS

DFS is excellent for path-related problems. As your interview prep guide from InterviewCrunch notes, these can be split into "validation" problems (does a path exist?) and "path" problems (what is the path?).

Graph Depth First Search - Coding Pattern

This resource from InterviewCrunch is designed specifically for interview patterns. It provides excellent templates for using DFS to solve pathing problems.

Read the sections 'Overview', 'Validation Problem', and 'Path Problem'. Pay close attention to the code templates. Notice how the recursive helper function is modified to handle each case.

Let's summarize these patterns.

Pattern 1: Path Validation

To check if a path exists between start and target, we modify our recursive DFS to return a boolean.

  • If curr == target, we've found it, so we return True.
  • The recursive call becomes: if any of my neighbors can find the target (if dfs(...)), return True.
  • If the loop finishes without finding the target, return False.

Here is the pattern from the article, adapted slightly for clarity:

def dfs_path_exists(curr, target, graph, visited):
    if curr in visited:
        return False
    visited.add(curr)
    
    if curr == target:
        return True
        
    for neighbor in graph.get(curr, []):
        if dfs_path_exists(neighbor, target, graph, visited):
            return True # Path found through this neighbor!
            
    return False # No path from this node

Pattern 2: Reconstructing the Path

This is a powerful pattern that previews the backtracking technique we'll cover later. The key is to pass a list representing the current path down the recursion.

  1. Append the current node to the path.
  2. If the current node is the target, you've found a path. Return True.
  3. Recursively call on neighbors. If any call returns True, it means a path was found, so propagate True up the call stack.
  4. If a recursive call returns False, it means that neighbor led to a dead end. You must backtrack by popping the node from the path before trying the next neighbor.

This "add, explore, remove" pattern is fundamental. The InterviewCrunch article provides a great template for this.


5. Complexity and When to Use DFS vs. BFS

  • Time Complexity:

    • Just like BFS, DFS visits every vertex () and every edge () once in the worst case.
  • Space Complexity:

    • For recursive DFS, the space is determined by the maximum depth of the recursion call stack. In the worst case (a long, chain-like graph), this can be .
    • For iterative DFS, the space is the maximum size of the explicit stack, which can also be in the worst case.

DFS vs. BFS - A Quick Guide for Interviews

Scenario Best Choice Why?
Shortest path in unweighted graph BFS Explores level by level, guaranteeing the first time it finds the target, it's via the shortest path.
Finding if any path exists DFS (often) Tends to find a path more quickly if one exists and is deep. The code is also arguably simpler recursively.
Topological Sort, Cycle Detection DFS DFS's ability to track the "state" of a node (visiting, visited) is ideal for detecting back-edges in cycles.
Exploring all nodes (e.g., connected components) Either Both will visit all nodes. The choice may depend on memory constraints or the graph's shape.
Fewest steps in a maze/grid BFS This is a shortest path problem in an unweighted graph.
Finding a solution in a maze DFS Good for finding a solution path, not necessarily the shortest. This mirrors backtracking problems.

Conclusion

You have now added the second major graph traversal algorithm to your toolkit. DFS is not just for traversal; it's the conceptual foundation for many advanced algorithms like topological sorting, cycle detection, and the entire backtracking paradigm.

Key Takeaways:

  • DFS explores deeply, following one path to its conclusion before backtracking.
  • It is powered by a stack, either the implicit function call stack (recursive) or an explicit one (iterative).
  • A visited set is crucial to handle cycles and avoid redundant work.
  • DFS can be adapted to solve path validation (does a path exist?) and path reconstruction problems.
  • The time complexity is , and space complexity is , related to the maximum path depth.

Preview of the Next Lesson:

In our next lesson, we will apply these traversal techniques to a very common interview pattern: grid-based graph problems. We'll tackle classics like "Number of Islands," where you'll see how to model a 2D grid as a graph and use DFS or BFS to find and count distinct groups of connected cells.