Skip to main content

Welcome to a Grasp lesson

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

Lesson 57

BFS for Graphs: A Queue-Based Approach

Recommended resources

Hello! Welcome to the third lesson in our module on Graph Traversal Algorithms.

In the last lesson, we delved into Depth-First Search (DFS), a traversal strategy that explores as far as possible along each branch before backtracking. We saw how its "deep-dive" nature could be elegantly implemented with recursion or, for more robustness, iteratively with a stack.

Today, we'll explore the other fundamental graph traversal algorithm: Breadth-First Search (BFS). Your goal for this lesson is to learn how to implement BFS for graphs using a queue. We'll see how swapping the stack (LIFO - Last-In, First-Out) used in iterative DFS for a queue (FIFO - First-In, First-Out) completely changes the exploration strategy. Instead of diving deep, BFS explores the graph layer by layer.

What is Breadth-First Search?

Imagine dropping a stone into a pond. The ripples spread out evenly in all directions, reaching all points at the same distance from the center at the same time. BFS works just like that. It starts at a source node and explores its immediate neighbors first. Then, it explores the neighbors of those neighbors, and so on. It systematically visits all nodes at a certain "level" or distance before moving on to the next level.

This level-by-level approach makes BFS incredibly useful for a specific and very common category of problems in competitive programming: finding the shortest path between two nodes in an unweighted graph.

To build a strong intuition for this, let's watch a short video that visualizes the BFS process.

Breadth First Search (BFS): Visualized and Explained

The video 'Breadth First Search (BFS): Visualized and Explained' by Reducible offers a fantastic analogy to help understand the core idea of BFS and why it naturally finds the shortest path.

Watch from the beginning to 02:38. Pay close attention to the 'balls with strings' analogy, as it perfectly illustrates the concept of levels and shortest paths.

As the video explained, the moment BFS reaches a node, it has found the shortest path from the source to that node (in terms of the number of edges). This is a guaranteed property of the algorithm.


The BFS Algorithm and the Role of the Queue

The heart of BFS is the queue. A queue is a data structure that follows a "First-In, First-Out" (FIFO) principle. Think of it like a line at a ticket counter—the first person to get in line is the first person to be served.

This FIFO behavior is what enables the level-by-level traversal. By adding a node's neighbors to the back of the queue, we ensure that we finish exploring all nodes at the current level before moving on to the nodes at the next level.

The algorithm proceeds as follows:

  1. Initialize: Create a queue and add the start_node. Create a visited set to track nodes we've already seen, and add the start_node to it.
  2. Loop: While the queue is not empty:
    a. Dequeue: Remove the node from the front of the queue. Let's call it current_node.
    b. Process: Do something with current_node (e.g., print it, check if it's the target).
    c. Explore Neighbors: For each neighbor of current_node:
    i. If the neighbor has not been visited:
    - Mark it as visited.
    - Add it to the back of the queue.

This process continues until the queue is empty, meaning all reachable nodes have been visited.

The following image provides a great step-by-step visualization of how the queue's contents change as the algorithm explores the graph.

Breadth-First Search (BFS) Algorithm Steps
This image breaks down the BFS process, showing how nodes are dequeued and their neighbors are enqueued, leading to a level-by-level traversal.

For a clear textual summary of these steps and the underlying pseudocode, the following article is a useful reference.

Breadth-First Search (BFS) Algorithm Explained

The article 'Breadth-First Search (BFS) Algorithm Explained' from Codecademy provides a concise, step-by-step breakdown of the algorithm.

Read the sections 'How breadth-first search works' and 'BFS algorithm pseudocode'. This will solidify your understanding of the formal steps before we jump into the Python code.


Implementing BFS in Python

Now, let's translate this algorithm into Python. We'll use the same adjacency list representation (defaultdict(list)) as in our previous lesson. For the queue, Python's collections.deque is the ideal tool. It's highly optimized for adding (append) and removing (popleft) elements from both ends.

The following video provides a detailed walkthrough of a complete BFS implementation, including tracking not just visited nodes, but also their levels (distance from the source) and parents (to reconstruct the path).

2.1 BFS: Breadth First Search Implementation in Python | Graph Data Structure

The video 'BFS: Breadth First Search Implementation in Python' by PyTech Vision walks through building a BFS function from scratch, explaining each component in detail.

Watch from 00:19 to 06:55. This covers setting up the graph, initializing the necessary data structures (visited, level, parent, queue), and the main BFS loop. You don't need to code along just yet, but focus on how each part of the algorithm is implemented.

Here is a clean, functional implementation that combines the core ideas from the resources.

from collections import defaultdict, deque

def bfs(graph, start_node):
    """
    Performs a Breadth-First Search on a graph.

    Args:
        graph (dict): The graph represented as an adjacency list.
        start_node: The node to start the traversal from.
    
    Returns:
        list: The order of nodes visited.
    """
    
    # Use a set for O(1) average time complexity for lookups
    visited = {start_node} 
    
    # Use deque for an efficient queue implementation
    queue = deque([start_node])
    
    traversal_order = []

    while queue:
        # Dequeue a vertex from the front of the queue
        node = queue.popleft()
        traversal_order.append(node)

        # Get all adjacent vertices of the dequeued vertex
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    
    return traversal_order

# --- Example Usage ---
# Build a graph
edges = [
    ('A', 'B'), ('A', 'C'),
    ('B', 'D'), ('B', 'E'),
    ('C', 'F'), ('E', 'F')
]
adj = defaultdict(list)
for u, v in edges:
    adj[u].append(v)
    adj[v].append(u) # For an undirected graph

# Perform BFS starting from node 'A'
bfs_path = bfs(adj, 'A')
print(f"BFS Traversal Order: {bfs_path}")

# Expected output might be: ['A', 'B', 'C', 'D', 'E', 'F']
# The exact order of nodes at the same level (e.g., B and C)
# depends on the order they were added to the adjacency list.

This image connects the code you just saw with a visualization of the data structures involved.

Breadth-First Search (BFS) in Python: Graph Visualization and Code
A helpful visualization connecting the Python code for BFS with the state of the `visited` set and the `queue` during traversal.

Application: Shortest Path and Distance

The most powerful application of BFS is finding the shortest path in an unweighted graph. We can easily augment our BFS function to calculate the distance (number of edges) from the source to all other nodes.

We do this by storing the level of each node. The source node is at level 0. Any neighbor of the source is at level 1. Any unvisited neighbor of a node at level k will be at level k+1.

Let's see how to implement this, and also how to find the actual path.

2.1 BFS: Breadth First Search Implementation in Python | Graph Data Structure

Continuing with the PyTech Vision video, these next segments show how to use the information gathered during BFS to find the shortest distance and reconstruct the path—a very common problem pattern.

Watch from 07:17 to 10:40. Focus on how the level and parent dictionaries are used after the BFS traversal is complete to answer questions about shortest distance and path.

Here's how you can modify the BFS function to return the distances and parent pointers:

def bfs_shortest_path(graph, start_node):
    visited = {start_node}
    queue = deque([(start_node, 0)]) # Queue now stores (node, level)
    
    levels = {start_node: 0}
    parents = {start_node: None}

    while queue:
        node, level = queue.popleft()

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                parents[neighbor] = node
                levels[neighbor] = level + 1
                queue.append((neighbor, level + 1))
    
    return levels, parents

# Using the same graph as before
levels, parents = bfs_shortest_path(adj, 'A')

# Find shortest distance to node 'F'
print(f"Shortest distance from A to F: {levels['F']}")

# Reconstruct the path from A to F
path = []
curr = 'F'
while curr is not None:
    path.append(curr)
    curr = parents[curr]
path.reverse() # The path is built backwards, so reverse it
print(f"Shortest path from A to F: {path}")

Output:

Shortest distance from A to F: 2
Shortest path from A to F: ['A', 'C', 'F']

This ability to find not just the distance but the path itself is a fundamental technique in your competitive programming toolkit.

Test your understanding!

Consider the following graph:

graph = {
    0: [1, 3],
    1: [0, 2],
    2: [1, 5],
    3: [0, 4],
    4: [3, 5],
    5: [2, 4]
}
  1. Starting from node 0, what is the BFS traversal order? (Assume neighbors are processed in the order they appear in the list).
  2. What is the shortest distance from node 0 to node 5?
Show answer
  1. BFS Traversal Trace:

    • queue = deque([0]), visited = {0}
    • Dequeue 0. Order: [0]. Enqueue 1, 3. queue = deque([1, 3]), visited = {0, 1, 3}.
    • Dequeue 1. Order: [0, 1]. Enqueue 2. queue = deque([3, 2]), visited = {0, 1, 3, 2}.
    • Dequeue 3. Order: [0, 1, 3]. Enqueue 4. queue = deque([2, 4]), visited = {0, 1, 3, 2, 4}.
    • Dequeue 2. Order: [0, 1, 3, 2]. Enqueue 5. queue = deque([4, 5]), visited = {0, 1, 3, 2, 4, 5}.
    • Dequeue 4. Order: [0, 1, 3, 2, 4]. Its neighbors 3 and 5 are visited.
    • Dequeue 5. Order: [0, 1, 3, 2, 4, 5]. Its neighbors 2 and 4 are visited.
    • Queue is empty. Traversal ends.
    • Final Order: [0, 1, 3, 2, 4, 5]
  2. Shortest Distance:

    • Level 0: {0}
    • Level 1: {1, 3} (neighbors of 0)
    • Level 2: {2, 4} (neighbors of 1 and 3)
    • Level 3: {5} (neighbor of 2 and 4)
    • The shortest distance from node 0 to node 5 is 3. The path could be 0 -> 1 -> 2 -> 5 or 0 -> 3 -> 4 -> 5.

Complexity Analysis

Just like DFS, the complexity of BFS is proportional to the number of vertices and edges in the graph.

  • Time Complexity: , where is the number of vertices and is the number of edges. This is because every vertex is enqueued and dequeued exactly once, and every edge is considered once.
  • Space Complexity: . In the worst-case scenario (a "star" graph where one central node is connected to all others), the queue could hold up to vertices at one time. The visited set also contributes to this space complexity.

Conclusion

You've now added Breadth-First Search to your algorithmic arsenal, a powerful technique for exploring graphs and a go-to method for shortest path problems on unweighted graphs.

Key Takeaways:

  • BFS explores a graph level by level, starting from a source node.
  • It uses a queue (FIFO) to manage the order of nodes to visit.
  • BFS is guaranteed to find the shortest path in terms of the number of edges in an unweighted graph.
  • By tracking levels and parents, you can find both the shortest distance and reconstruct the path itself.
  • The time and space complexities are and respectively, the same as DFS.

Up Next:
In the following lessons, we'll start applying these traversal algorithms to solve more complex problems. Our next topic will be detecting cycles in graphs using DFS, which is a classic application of the depth-first traversal pattern.