Skip to main content

Welcome to a Grasp lesson

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

Lesson 22

BFS: Implementation & Shortest Path Applications

Recommended resources

Hello! Welcome to your next lesson on graph algorithms.

In our last session, we explored Depth-First Search (DFS), a traversal strategy that goes as deep as possible down one path before backtracking. You learned how it uses a stack (either the program's call stack or an explicit one) to manage this "deep dive."

Today, we'll look at the other fundamental traversal algorithm: Breadth-First Search (BFS). Your learning outcome is to implement BFS and understand its applications, particularly for finding the shortest path in unweighted graphs. While DFS plunges into the graph, BFS explores it layer by layer, like ripples expanding in a pond. This simple change in strategy, powered by a different data structure, unlocks a powerful new capability.

1. The Core Idea: Exploring Layer by Layer

Imagine you are at the center of a social network and want to find the connection path with the fewest "friend-of-a-friend" hops to another person. You wouldn't follow one long chain of connections. Instead, you'd check all your direct friends (level 1), then all their friends (level 2), and so on, until you find the person.

This is the exact intuition behind Breadth-First Search. It starts at a source node and explores all its immediate neighbors first, before moving on to the next level of neighbors.

The key to achieving this level-by-level exploration is using a Queue, which operates on a First-In, First-Out (FIFO) principle. This is the fundamental difference from DFS, which uses a Last-In, First-Out (LIFO) stack.

To see this in action, let's start with a video that visualizes the process.

Breadth First Search Algorithm | Shortest Path | Graph Theory

This video by WilliamFiset provides an excellent animation of how BFS explores a graph in layers, clearly showing the role of the queue.

Watch from 00:54 to 03:12. Focus on how nodes are added to the queue and processed in the order they are discovered, resulting in the layered traversal.

As you saw, the queue ensures that we fully explore all nodes at a given distance (or "level") from the source before moving to nodes at the next level.

Breadth-First Search (BFS) Step-by-Step Visualization
This image illustrates the level-by-level exploration of BFS. Starting from a source node, it visits all nodes at level 1, then all nodes at level 2, and so on, guaranteeing that it finds paths in increasing order of length.

2. The BFS Algorithm and Complexity

The formal algorithm for BFS is quite straightforward and builds on concepts you're already familiar with from DFS, like the visited set.

The steps are:

  1. Initialize a queue and add the start_node to it.
  2. Initialize a visited set and add the start_node to it.
  3. While the queue is not empty:
    a. Dequeue a node (let's call it current_node).
    b. Process current_node.
    c. For each neighbor of current_node:
    i. If the neighbor has not been visited:
    - Add it to the visited set.
    - Enqueue the neighbor.

Breadth-First Search (BFS)

For a clear, textual breakdown of the algorithm's steps, terminology, and complexity, this article from CelerData is a great resource.

Read the sections 'What Is Breadth-First Search (BFS)?' and 'Complexity Analysis'. This will formalize the concepts we've just discussed.

Complexity Analysis

As noted in the article, the complexity of BFS is identical to DFS:

  • Time Complexity:

    • Each vertex is enqueued and dequeued exactly once, which is .
    • Every edge is examined once (for directed graphs) or twice (for undirected graphs) when we check the neighbors of each vertex. This is .
    • The total time is therefore linear in the size of the graph.
  • Space Complexity:

    • The visited set can store up to vertices.
    • The queue, in the worst-case scenario (a very wide, shallow graph), might need to hold nearly all the vertices at once. For instance, in a star graph, after visiting the center node, all other nodes are added to the queue. Therefore, the space complexity is .

3. Application: Shortest Path in Unweighted Graphs

Here is the most significant application of BFS. Because it explores level by level, BFS is guaranteed to find the shortest path from a source node to all other nodes in an unweighted graph. The "shortest path" here means the path with the minimum number of edges.

The first time BFS reaches a target node, it is guaranteed to be via a shortest path. Why? Because if a shorter path existed, BFS would have discovered the target node at an earlier level.

To implement this, we can modify our algorithm slightly. Instead of just a visited set, we'll use a distance array. This array will store the shortest distance from the source to every other node.

Let's watch a detailed walkthrough of this process.

G-28. Shortest Path in Undirected Graph with Unit Weights

The channel 'take U forward' provides a fantastic, step-by-step example of using BFS to calculate the shortest distances from a source node.

Watch from the beginning to 11:25. Pay close attention to how the distance array is initialized (with infinity) and updated each time a new, shorter path to a node is found.

Reconstructing the Path

Knowing the shortest distance is useful, but often you need the actual path. To do this, we need to track the "parent" of each node—that is, the node from which it was discovered. We can use a previous or parent array for this.

When BFS adds a neighbor to the queue, we record that the current_node is the parent of that neighbor. Once the BFS is complete (or the target is found), we can reconstruct the path by starting at the target node and backtracking through the parent pointers until we reach the source.

The following video segments explain this path reconstruction logic.

Breadth First Search Algorithm | Shortest Path | Graph Theory

Let's return to the WilliamFiset video, which clearly explains the pseudocode for both the main search loop and the path reconstruction step using a prev array.

Watch from 03:12 to 07:16. Focus on how the prev array is populated during the search and then used in reverse to build the final path.

This combination of finding distances and then reconstructing the path is a powerful technique that forms the basis for more complex shortest path algorithms like Dijkstra's, which you'll encounter later.

Test your understanding!

Consider this unweighted, undirected graph represented by an adjacency list:

  • A: [B, D]
  • B: [A, C, E]
  • C: [B, F]
  • D: [A, E]
  • E: [B, D, F]
  • F: [C, E, G]
  • G: [F]

Using BFS, find the shortest path from node A to node F. What is the path and what is its length (number of edges)? Assume neighbors are explored in alphabetical order.

Show answer

The shortest path is A -> B -> E -> F and its length is 3.

Let's trace the BFS:

  1. Queue: [A], Distances: {A: 0}, Parents: {}
  2. Dequeue A. Process neighbors B, D.
    • Enqueue B. Distances: {A: 0, B: 1}, Parents: {B: A}
    • Enqueue D. Distances: {A: 0, B: 1, D: 1}, Parents: {B: A, D: A}
    • Queue: [B, D]
  3. Dequeue B. Neighbors are A, C, E. (A is visited).
    • Enqueue C. Distances: {..., C: 2}, Parents: {..., C: B}
    • Enqueue E. Distances: {..., E: 2}, Parents: {..., E: B}
    • Queue: [D, C, E]
  4. Dequeue D. Neighbors are A, E. (A is visited, E is visited). No change.
    • Queue: [C, E]
  5. Dequeue C. Neighbors are B, F. (B is visited).
    • Enqueue F. Distances: {..., F: 3}, Parents: {..., F: C}
    • Queue: [E, F]
  6. Dequeue E. Neighbors are B, D, F. (B and D are visited).
    • We check F. Current distance to F is 3 (via C). The path via E would be A->B->E->F, also length 3. Since we don't update for equal length paths, nothing changes.
    • Queue: [F]
  7. Dequeue F. We have reached our target!

Path Reconstruction:

  • Start at F. Parent of F is C. Path: F <- C
  • Parent of C is B. Path: F <- C <- B
  • Parent of B is A. Path: F <- C <- B <- A
  • Reverse the path: A -> B -> C -> F. Length: 3.

Wait, did you get A -> B -> E -> F? Let's re-trace.

  • Step 5: When we processed C, we found a path to F of length 3. A->B->C->F. We marked parent[F] = C.
  • Step 6: When we processed E, its neighbor F was already visited. The rule is, we don't re-add visited nodes to the queue. So, the first path found is the one reported.

The path A -> B -> E -> F is also a valid shortest path of length 3. The specific path found depends on the order neighbors are processed. If we process E's neighbors before C's neighbors (e.g., if E was earlier in the queue than C), we might find that path first. Both are correct shortest paths. My trace found A -> B -> C -> F.


4. Implementation in Practice

As a software engineer, translating this theory into code is the final step. The resources below provide a path from a basic Graph class to a full implementation of BFS for finding shortest paths.

Breadth-First Search

This lab guide from BYU is excellent. It walks you through building a Python Graph class and then adding a BFS method to it, followed by a method to find the shortest path.

Read the sections 'Breadth-First Search' (the core algorithm), 'Problem 2' (implementing traversal), 'Shortest Paths via BFS', and 'Problem 3' (implementing shortest path). You don't have to code it now, but understand how the methods would be structured within a class.

The guide's approach of encapsulating graph logic within a class is a common and effective software engineering practice.


Conclusion

Today you've added another fundamental tool, Breadth-First Search, to your algorithmic toolkit. You've seen how a simple switch from a stack (DFS) to a queue (BFS) changes the entire exploration strategy from "deep" to "wide."

Key Takeaways:

  • BFS explores level-by-level using a queue (FIFO) to maintain the order of nodes to visit.
  • Its time complexity is and space complexity is , just like DFS.
  • The key application of BFS is finding the shortest path in unweighted graphs, a problem it solves optimally due to its layered traversal.
  • Path reconstruction is done by tracking each node's parent during the traversal and backtracking from the target.

Preview of the Next Lesson:
We've now covered the two main ways to traverse a graph. In the next lesson, we will revisit DFS to see how we can augment it to solve another critical problem: detecting cycles in both directed and undirected graphs. This is a crucial capability for tasks like validating dependencies, finding deadlocks, and ensuring a graph has properties required for other algorithms (like topological sorting).