Hello! Welcome to your third lesson in the Graph Algorithms module.
In our last session, we mastered Breadth-First Search (BFS). We saw how its level-by-level exploration, powered by a queue, naturally finds the shortest path in an unweighted graph. This "wide" exploration is powerful, but it's not the only way to traverse a graph.
Today, we dive into the other foundational traversal algorithm: Depth-First Search (DFS). Where BFS explores cautiously in expanding ripples, DFS is an aggressive explorer, venturing as deep as possible down a single path before backtracking. This simple change in strategy unlocks a completely different set of applications. Our goal today is to implement Depth-First Search (DFS) recursively and iteratively for traversal and cycle detection.
The Five Concepts for Today
We will break down DFS into five interconnected concepts, building from the core idea to advanced, interview-critical applications:
- The Core Idea of DFS: The philosophy of "go deep."
- Recursive DFS: The elegant implementation using the call stack.
- Iterative DFS: The robust, production-safe implementation with an explicit stack.
- Application 1: Cycle Detection in Undirected Graphs.
- Application 2: Cycle Detection in Directed Graphs (The 3-Color Method).
Concept 1: The Core Idea of DFS
Technical Explanation
Depth-First Search (DFS) is a graph traversal algorithm that starts at a source node and explores as far as possible along each branch before backtracking. Unlike BFS which uses a First-In, First-Out (FIFO) queue, DFS uses a Last-In, First-Out (LIFO) strategy, which is naturally implemented with a stack.
The algorithm's philosophy is:
- Visit a node.
- Pick one of its unvisited neighbors and immediately go to it.
- Repeat this process, diving deeper and deeper into the graph.
- Once you hit a "dead end" (a node with no unvisited neighbors), you backtrack to the previous node and try another one of its unvisited neighbors.
This process continues until all reachable nodes have been visited. The path it takes is often long and winding, in stark contrast to the systematic layers of BFS.
Depth First Search (DFS) Explained: Algorithm, Examples, and Code
To build a strong intuition for this "go deep" philosophy, watch the first part of the 'Depth First Search (DFS) Explained' video from the Reducible channel. It provides an excellent, animated walkthrough of the exploration process.
Watch from the beginning until 06:21. Focus on the analogy of the 'bold and adventurous explorer' and how the algorithm retraces its steps from dead ends.
Metaphor: Solving a Maze
Imagine you're solving a maze using the "right-hand rule": you keep your right hand on a wall and just keep walking.
- You will follow one corridor as far as it goes (diving deep).
- When you hit a dead end, you turn around and retrace your steps until you find an unexplored fork in the path (backtracking).
- You then take that new path, again following it to its conclusion.
This is exactly how DFS explores a graph. The stack acts as your memory of which forks you have yet to try.
Python Code: Basic Traversal Order
This simple snippet demonstrates the path DFS takes. We'll use recursion here for simplicity, which we will formalize in the next concept.
def dfs_traversal_simple(graph, node, visited=None):
"""
Performs a simple DFS traversal to show the order of visited nodes.
"""
if visited is None:
visited = set()
if node not in visited:
print(node, end=" ") # Process the node
visited.add(node)
for neighbor in graph.get(node, []):
dfs_traversal_simple(graph, neighbor, visited)
# Example graph from previous lesson
graph_example = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
print("DFS Traversal Order (can vary based on neighbor order):")
dfs_traversal_simple(graph_example, 'A')
# Expected Output could be: A B D E F C
# Note how it goes A->B->D (deep) before ever visiting C.
Concept 2: Recursive DFS Implementation
Technical Explanation
The most intuitive way to implement DFS is using recursion. The function call stack itself acts as the LIFO stack we need.
The logic is beautifully simple:
- Define a function
dfs(node, visited). - The base case: If
nodeis already in thevisitedset,return. - Mark the current
nodeas visited. - Process the node (e.g., add it to a results list).
- For each
neighborofnode, make a recursive call:dfs(neighbor, visited).
The call stack handles the backtracking automatically. When a recursive call for a path finishes (hits a dead end), the program execution returns to the previous function call in the stack, which can then proceed to its next neighbor.
Depth First Search (DFS) Explained: Algorithm, Examples, and Code
The 'Reducible' video continues by detailing this exact recursive implementation. It excellently visualizes how the call stack grows and shrinks, making the 'magic' of recursion clear.
Watch from 06:21 to 11:10. Pay close attention to how the visited set prevents infinite recursion in graphs with cycles.
Metaphor: A Chain of Command
Imagine a general (start_node) giving an order.
- The general tells a colonel (
neighbor 1): "Scout this region." - The colonel tells a captain (
neighbor 1.1): "Scout this sector." - This continues down to a private who scouts a single point and reports back.
- Once the captain has received reports from all their soldiers, they report back to the colonel.
- Once the colonel has reports from all their captains, they report back to the general.
The chain of command is the call stack. A report going back up the chain is a function return.
Python Code: Recursive Implementation
This is a more structured implementation, commonly seen in interviews.
def dfs_recursive(graph: dict, start_node: str):
"""
Performs a DFS traversal using recursion.
Returns:
A list of nodes in one possible DFS traversal order.
"""
visited = set()
path = []
def _dfs(node):
if node in visited:
return
visited.add(node)
path.append(node) # Process node
# Explore neighbors
for neighbor in graph.get(node, []):
_dfs(neighbor)
_dfs(start_node)
return path
# Using the same graph
path_rec = dfs_recursive(graph_example, 'A')
print(f"\nRecursive DFS Path: {path_rec}")
Concept 3: Iterative DFS Implementation
Technical Explanation
While elegant, recursion has a major flaw: it can cause a stack overflow error if the graph is too deep (e.g., a long chain of nodes). Every function call consumes memory on the call stack, which has a limited size. For production systems at companies like Google or Meta, where input sizes can be massive, relying on recursion is a risk.
The solution is an iterative implementation using an explicit stack data structure.
The logic:
- Initialize a
stackwith thestart_node. - Initialize a
visitedset. - Loop while the
stackis not empty:
a. Pop anodefrom the stack.
b. Ifnodehas been visited,continue.
c. Marknodeas visited and process it.
d. For eachneighborofnode, push it onto the stack.
This achieves the same LIFO behavior without deep recursion, making it robust and scalable.

DFS Iterative vs Recursive: When to Use Each and Why It Matters
For a detailed breakdown of the pros and cons, the article 'DFS Iterative vs Recursive' is an excellent guide. It frames the choice in the context of interviews and production code, which aligns perfectly with your goals.
Read the sections on 'DFS Recursive', 'DFS Iterative', and the comparison table under 'When to Choose DFS Iterative vs Recursive'. This will solidify your understanding of the trade-offs, a key discussion point in senior interviews.
Python Code: Iterative Implementation
Notice we often add neighbors to the stack in reverse order to match the traversal order of the typical recursive implementation. This is a common interview trick.
def dfs_iterative(graph: dict, start_node: str):
"""
Performs a DFS traversal using an explicit stack.
Returns:
A list of nodes in one possible DFS traversal order.
"""
stack = [start_node]
visited = set()
path = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
path.append(node)
# Add neighbors to the stack. Adding them in reverse order of the adjacency
# list ensures that the first neighbor is processed first, mimicking recursion.
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append(neighbor)
return path
path_iter = dfs_iterative(graph_example, 'A')
print(f"Iterative DFS Path: {path_iter}")
# With the reversed loop, this should produce the same order as the recursive one.
Concept 4: Application 1: Cycle Detection in an Undirected Graph
Technical Explanation
A primary use of DFS is detecting cycles. In an undirected graph, a cycle exists if, during traversal, we encounter a visited node that is not the immediate parent of the current node.
Why the "not the parent" condition? In an undirected graph, the edge (A, B) means we can go A -> B and B -> A. If we just checked for any visited neighbor, we'd immediately find A when exploring from B, incorrectly flagging a cycle.
The algorithm is a small modification to our recursive DFS:
- The
dfsfunction now takes(node, parent). - When exploring from
current_node, iterate through itsneighbors. - If a
neighboris already visited andneighboris not theparentofcurrent_node, we have found a back edge. This indicates a cycle. - If a
neighboris not visited, recurse:dfs(neighbor, current_node).
The article 'Detect Cycle in a Graph' clearly explains this parent-tracking technique for undirected graphs.
Read the section 'Detect Cycle in an Undirected Graph'. Focus on why the white-grey-black method (which we'll see next) doesn't work here and how tracking the parent solves the problem.
Metaphor: One-Way Memory
You're the maze solver again. You have a very short-term memory: you only remember the single chamber you just came from.
- You enter a new chamber.
- You look at the connecting tunnels. One tunnel leads to a chamber you've already visited.
- If that chamber is the one you just came from, that's fine, you just won't go back.
- But if that chamber is not the one you just came from, it means there's another way to get to it. You've found a loop!
Python Code: Undirected Cycle Detection
def has_cycle_undirected(graph: dict) -> bool:
"""
Checks for a cycle in an undirected graph using DFS.
"""
visited = set()
def _dfs(node, parent):
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor == parent:
continue # Don't go back to the immediate parent
if neighbor in visited:
return True # Found a back edge to a visited, non-parent node
if _dfs(neighbor, node): # Recurse
return True
return False
# Handle disconnected graphs by trying to start a DFS from every node
for node in graph:
if node not in visited:
if _dfs(node, None):
return True
return False
# Example Graphs
undirected_cyclic = {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}
undirected_acyclic = {'A': ['B'], 'B': ['A', 'C'], 'C': ['B']}
print(f"Graph 1 has cycle: {has_cycle_undirected(undirected_cyclic)}") # Expected: True
print(f"Graph 2 has cycle: {has_cycle_undirected(undirected_acyclic)}") # Expected: False
Test your understanding!
Consider a graph represented by {'A': ['B'], 'B': ['A']}. Is this graph cyclic according to our definition? Why is the parent check essential here?
Show answer
No, this graph is not considered to have a cycle. It's just a single edge between two nodes in an undirected graph.
The parent check is essential because if we start a DFS from A, we visit B. From B, one of its neighbors is A, which is already in the visited set. Without the parent check, we would incorrectly flag this as a cycle. By checking if neighbor == parent, we see that A is the parent of B in this traversal path, so we correctly ignore it and conclude there is no cycle.
Concept 5: Application 2: Cycle Detection in a Directed Graph
Technical Explanation
Detecting cycles in a directed graph is more complex. The simple parent check doesn't work. Consider A -> B and A -> C -> B. When we are at B, we might see a path from a visited node C, but that doesn't form a cycle. A cycle exists only if we find a path from a node back to one of its own ancestors in the current traversal path.
This requires a three-state system, often called the white-grey-black method:
- WHITE: Node has not been visited at all.
- GREY: Node is currently being visited. It's in our current recursion stack. These are the "ancestors".
- BLACK: Node and all its descendants have been fully explored.
The algorithm:
- Initialize all nodes to WHITE.
- Start a DFS from any WHITE node. When you first visit a node, change its color to GREY.
- For each neighbor:
- If the neighbor is GREY, you've found a back edge to an ancestor. A cycle exists.
- If the neighbor is WHITE, recurse on it.
- After exploring all descendants of a node (i.e., after the recursive calls for all its neighbors have returned), change its color to BLACK.

This method is a classic and critical interview topic. The 'Detect Cycle in a Graph' article provides the best explanation.
Read the section 'Detect Cycle in a Directed Graph'. Understand the roles of the three colors and why encountering a 'grey' node signals a cycle.
Python Code: Directed Cycle Detection (3-Color Method)
def has_cycle_directed(graph: dict) -> bool:
"""
Checks for a cycle in a directed graph using 3-color DFS.
"""
# 0: white (unvisited), 1: grey (visiting), 2: black (visited)
colors = {node: 0 for node in graph}
def _dfs(node):
colors[node] = 1 # Mark as grey (visiting)
for neighbor in graph.get(node, []):
if colors.get(neighbor) == 1: # Neighbor is grey -> cycle detected
return True
if colors.get(neighbor) == 0: # Neighbor is white
if _dfs(neighbor):
return True
colors[node] = 2 # Mark as black (finished)
return False
for node in graph:
if colors[node] == 0:
if _dfs(node):
return True
return False
# Example Graphs
directed_cyclic = {'A': ['B'], 'B': ['C'], 'C': ['A']}
directed_acyclic = {'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': []}
print(f"Directed Graph 1 has cycle: {has_cycle_directed(directed_cyclic)}") # Expected: True
print(f"Directed Graph 2 has cycle: {has_cycle_directed(directed_acyclic)}") # Expected: False
Conclusion
Today you've mastered Depth-First Search, the powerful counterpart to BFS. You've learned how its "go deep" strategy, powered by a stack, leads to a different kind of exploration.
Key Takeaways:
- DFS explores deeply using a LIFO strategy, implemented with a stack (either the implicit call stack or an explicit one).
- Recursive DFS is elegant and concise but risks stack overflow on deep graphs.
- Iterative DFS is more verbose but robust and production-safe, giving you full control over memory.
- Cycle detection is a key application. In undirected graphs, you look for a visited neighbor that isn't your immediate parent. In directed graphs, you use the more robust white-grey-black three-state method to find back edges to ancestors.
- Time complexity for all variants is and space complexity is , same as BFS.
Preview of the Next Lesson:
The ability to detect cycles in a directed graph is not just an academic exercise. It's the prerequisite for one of the most important graph algorithms: Topological Sort. In our next lesson, we will see how we can use DFS to linearly order the vertices of a Directed Acyclic Graph (DAG), an algorithm with profound applications in task scheduling, dependency resolution (like in compilers and build systems), and data processing pipelines.