Welcome to the next lesson in our "Data Structures & Algorithms" module. In our last session, we dissected Quicksort, analyzing its powerful in-place partitioning strategy and its performance trade-offs against Merge Sort. This gave us a deep appreciation for how different algorithmic approaches can have significant practical implications.
Today, we shift our focus from sorting disconnected items to exploring interconnected data. We will tackle two of the most fundamental algorithms in computer science: Breadth-First Search (BFS) and Depth-First Search (DFS). These are the primary methods for traversing graphs and trees, data structures that model everything from social networks and computer networks to file systems and dependency chains. Your goal is to understand how each algorithm works, learn to implement them, and, most importantly, know when to apply one over the other to solve a given problem.
Graphs: The Language of Connection
Before we can traverse a graph, we need a common language to describe one. A graph is simply a collection of nodes (or vertices) connected by edges. Many real-world engineering problems can be modeled this way.
The video below offers an excellent, concise introduction to graph terminology and the common ways we represent them in code. The most important representation for our purposes today is the adjacency list.
Graphs: Edge List, Adjacency Matrix, Adjacency List, DFS, BFS - DSA Course in Python Lecture 11
This video from Greg Hogg's DSA course clearly explains graph basics and compares the three main ways to represent them programmatically.
Please watch the following two segments: Graph Terminology: This covers the essential concepts like vertices, edges, and directed vs. undirected graphs. Graph Representations: Pay close attention to the comparison between an edge list, adjacency matrix, and adjacency list. Understand why the adjacency list is often the most practical choice for traversal algorithms, as it directly maps a node to its neighbors.
An adjacency list is efficient because it gives us immediate access to a node's neighbors. In practice, this is often implemented using a hash map (or dictionary in Python), where each key is a node and its value is a list of its adjacent nodes.
This article by Jake Tae demonstrates a clean and simple setup for an adjacency list using a Python dictionary.
Read the Setup section to see how a graph can be represented with a hash map. This is the data structure we'll assume for the rest of this lesson.
The Two Traversal Strategies: Wide vs. Deep
BFS and DFS are two distinct strategies for visiting every node in a graph. The fundamental difference lies in the order they explore nodes.
- Breadth-First Search (BFS) explores layer by layer. It visits all of a node's immediate neighbors before moving on to the next level of neighbors. Think of it as the ripples spreading from a stone dropped in a pond.
- Depth-First Search (DFS) explores as far as possible along one path before backtracking. It dives deep into a branch, and only when it hits a dead end does it return to explore an alternative path. Think of this as navigating a maze.
The image below provides a clear visual comparison of their traversal orders on the same graph structure.
.png)
Breadth-First Search (BFS): The Shortest Path Finder
Because BFS expands level by level, it has a very important property: in an unweighted graph, it naturally finds the shortest path from the starting node to all other nodes. This makes it invaluable for problems like finding the shortest route in a network or the fewest steps to solve a puzzle.
To achieve this level-by-level traversal, BFS uses a Queue, which follows a First-In, First-Out (FIFO) principle.
The algorithm works as follows:
- Initialize a
queueand add the starting node. - Create a
visitedset and add the starting node to it to avoid cycles and redundant work. - While the
queueis not empty:
a. Dequeue a node (let's call itcurrent).
b. Processcurrent.
c. For each neighbor ofcurrent:
i. If the neighbor has not been visited, add it to thevisitedset and enqueue it.
The following video from Abdul Bari provides an excellent, detailed walkthrough of this process.
5.1 Graph Traversals - BFS & DFS -Breadth First Search and Depth First Search
This classic lecture clearly demonstrates the BFS algorithm using a queue and builds the resulting "spanning tree" step by step.
Watch the detailed explanation of Breadth-First Search. Pay close attention to how nodes are added to and removed from the queue, and how that corresponds to the level-by-level exploration.
Depth-First Search (DFS): The Path Explorer
DFS is the algorithm of choice for problems where you need to explore every possible path, such as maze-solving, finding connected components in a network, or checking for cycles. It's also the foundation for more advanced algorithms like backtracking.
To achieve its deep-diving behavior, DFS uses a Stack, which follows a Last-In, First-Out (LIFO) principle. In many cases, this stack is managed implicitly by the program's call stack when using a recursive implementation.
The algorithm can be implemented both iteratively and recursively.
Iterative DFS (with an explicit stack):
- Initialize a
stackand add the starting node. - Create a
visitedset. - While the
stackis not empty:
a. Pop a node from the stack (current).
b. Ifcurrenthas been visited, skip to the next iteration.
c. Markcurrentas visited and process it.
d. For each neighbor ofcurrent, push it onto the stack.
Recursive DFS:
- Define a function
dfs(node, visited_set). - Mark the
nodeas visited and process it. - For each neighbor of
node:
a. If the neighbor has not been visited, recursively calldfs(neighbor, visited_set).
The following video provides a very clear explanation of both the recursive and iterative approaches to DFS.
Graphs: Edge List, Adjacency Matrix, Adjacency List, DFS, BFS - DSA Course in Python Lecture 11
Let's return to Greg Hogg's video for a practical demonstration of DFS.
Watch the segment on Depth-First Search. The video does a great job of visualizing both the recursive (implicit stack) and iterative (explicit stack) versions and highlights the critical role of the seen set for handling cycles.
Implementation and Problem-Solving Patterns
Now that you have the conceptual foundation, let's look at how to implement these algorithms and, more importantly, when to use them.
This article provides clean Python implementations for both algorithms and shows how they can be applied to path-finding problems.
Please read the following sections: Breadth First Search: Review the Python code for bfs and note the discussion on using collections.deque for an efficient queue. Depth First Search: Examine both the iterative (dfs) and recursive (dfs_recursive) implementations. Path Finding: This is a key section. It demonstrates how to adapt both algorithms to find a path between two nodes and reinforces why BFS is the natural choice for the shortest path.
As an engineering manager, one of the most valuable skills is quickly recognizing which tool is right for a given job. The following resource provides an excellent mental checklist for deciding between DFS and BFS.
This guide from DevWeekends focuses on pattern recognition, which is perfect for applying these concepts in practice.
Please review two key parts of this resource: The Pattern Recognition Checklist. This gives you a fast way to map problem requirements (e.g., "find shortest path," "detect cycles") to the correct algorithm. The DFS vs BFS Quick Reference table at the end. This is a perfect summary of the core trade-offs.
Complexity Analysis
Finally, let's briefly touch on the performance of these algorithms. Let be the number of vertices and be the number of edges in the graph.
-
Time Complexity: For both BFS and DFS, the time complexity is . This is because, in the worst case, we must visit every vertex () and traverse every edge () exactly once.
-
Space Complexity: This is where the two algorithms differ significantly.
- BFS: The space complexity is proportional to the maximum width of the graph, . In a "bushy" graph that expands rapidly, the queue can hold many nodes at a single level. In the worst case of a complete graph, this can be .
- DFS: The space complexity is proportional to the maximum depth of the graph, . For a "long and skinny" graph, the recursion stack can become very deep. In the worst case, this can also be .
The choice between them can sometimes be dictated by memory constraints if the graph has a known shape (e.g., very wide vs. very deep).
Conclusion
In this lesson, we've explored the two fundamental workhorses of graph traversal. You now have the knowledge to navigate connected data structures efficiently.
Here are the essential takeaways:
- BFS (Breadth-First Search):
- Strategy: Explores level by level, "wide" before "deep."
- Data Structure: Queue (FIFO).
- Best For: Finding the shortest path in an unweighted graph, level-order tree traversal.
- DFS (Depth-First Search):
- Strategy: Explores one path to its conclusion before backtracking.
- Data Structure: Stack (LIFO), often implemented with recursion.
- Best For: Checking for path existence, detecting cycles, exploring all possibilities (e.g., solving a maze), and as a foundation for backtracking.
- Performance: Both have a time complexity of . Their space complexity depends on the graph's shape—BFS is sensitive to width (), while DFS is sensitive to height ().
In our next lesson, we will venture into a new and powerful algorithmic paradigm: dynamic programming. You will learn how to solve the classic 0/1 Knapsack problem, which will introduce you to techniques for breaking down complex optimization problems into simpler, overlapping subproblems.