← Back to all blogs

Comparing Queue and List Implementations for Breadth-First Search (BFS)

| November 4, 2024

TL;DR

Breadth-First Search (BFS) can be implemented using either a queue or a list in Python. However, using a queue (from collections.deque) is much more efficient for BFS due to its O(1) time complexity for both appending and popping from the front. In contrast, a list has O(n) complexity when removing elements from the front, which can significantly impact performance in large graphs. This post provides a reusable Graph class to handle BFS more effectively.

Table of Contents

Overview of BFS

Breadth-First Search explores the nodes and edges of a graph in layers, starting from a given node and visiting all of its neighbors before moving on to the neighbors’ neighbors. This process continues until all reachable nodes have been explored.

Implementing a Reusable Graph Class

To make our BFS implementation more reusable, let’s define a Graph class. This class will include methods for adding edges and performing BFS with either a queue or a list.

Graph Class with BFS Methods

from collections import deque
class Graph:
def __init__(self):
self.graph = {}
def add_edge(self, u, v):
"""Adds a directed edge from node u to node v."""
if u not in self.graph:
self.graph[u] = []
self.graph[u].append(v)
def bfs_with_queue(self, start):
"""BFS using a queue (deque) for efficient FIFO operations."""
visited = set()
queue = deque([start])
while queue:
vertex = queue.popleft()
if vertex not in visited:
print(vertex, end=" ")
visited.add(vertex)
queue.extend(neighbor for neighbor in self.graph.get(vertex, []) if neighbor not in visited)
def bfs_with_list(self, start):
"""BFS using a list, which has less efficient FIFO operations."""
visited = set()
queue = [start]
while queue:
vertex = queue.pop(0) # O(n) operation for large lists
if vertex not in visited:
print(vertex, end=" ")
visited.add(vertex)
queue.extend(neighbor for neighbor in self.graph.get(vertex, []) if neighbor not in visited)

Example Usage

Let’s create a sample graph and run BFS using both the bfs_with_queue and bfs_with_list methods.

# Initialize the graph and add edges
graph = Graph()
graph.add_edge('A', 'B')
graph.add_edge('A', 'C')
graph.add_edge('B', 'D')
graph.add_edge('B', 'E')
graph.add_edge('C', 'F')
graph.add_edge('E', 'F')
print("BFS using Queue:")
graph.bfs_with_queue('A')
print("\nBFS using List:")
graph.bfs_with_list('A')

Performance Comparison

  1. Efficiency:

    • Queue (deque): The deque implementation provides O(1) time complexity for appending and popping from both ends, making it suitable for BFS.
    • List: Using pop(0) on a list has O(n) time complexity because it requires shifting all other elements. This can lead to performance issues for larger graphs.
  2. Memory Usage: Both implementations have similar memory usage, but the queue is generally more efficient for larger data sets due to reduced operation overhead.

  3. Code Clarity: The deque-based implementation is more intuitive and cleaner, aligning closely with the BFS algorithm’s FIFO nature.

Conclusion

Using a Graph class with a queue (implemented with deque) for BFS is generally the best approach in Python for both performance and code readability. Lists are less efficient for BFS due to their O(n) time complexity when removing elements from the front. By encapsulating BFS logic in a Graph class, we can easily reuse and extend our code for other graph-based operations.

Try using this class with different graph structures to see how each BFS method performs!