Exploring Graphs with Breadth-First Search in Python

Introduction

Graphs are an essential data structure used in computer science and various real-world applications. They model relationships between entities and help solve problems such as finding the shortest path, network analysis, social network connections, and more. One fundamental graph traversal algorithm is Breadth-First Search (BFS), which systematically explores all the vertices and edges of a graph starting from a given source vertex. In this article, we’ll delve into the concepts and implementation of Breadth-First Search in Python.

Understanding Breadth-First Search

Breadth-First Search is a graph traversal algorithm that visits all the vertices of a graph level by level, moving outward from the starting vertex. The primary idea behind BFS is to explore the nearest neighbors of a node before moving on to their neighbors, thus ensuring that the algorithm visits vertices in increasing order of their distance from the source.

The algorithm utilizes a queue data structure to keep track of vertices that need to be visited. The process starts by enqueueing the source vertex into the queue and marking it as visited. While the queue is not empty, the algorithm dequeues a vertex, explores its neighbors, and enqueues any unvisited neighbors. By continuously repeating this process, BFS guarantees that all vertices are visited, and the shortest path to each vertex from the source is found.

Visual Representation of Breath-First-Search

The following steps will assist you in grasping the workings of Breath-First-Search by providing an example with a Graph.

Traverse A

A graph representation with nodes and edges. The traversal starts from the first node, and enqueue and dequeue operations are performed using an array-based data structure.

Traverse B

A graph representation with nodes and edges. Traversing the B node, and enqueue and dequeue operations are performed using an array-based data structure.

Traverse C

A graph representation with nodes and edges. Traversing the C node, and enqueue and dequeue operations are performed using an array-based data structure.

Traverse D

A graph representation with nodes and edges. Traversing the D node, and enqueue and dequeue operations are performed using an array-based data structure.

Traverse E

A graph representation with nodes and edges. Traversing the E node, and enqueue and dequeue operations are performed using an array-based data structure.

Traverse F

A graph representation with nodes and edges. Traversing the last node, and enqueue and dequeue operations are performed using an array-based data structure.

Python Implementation of Breadth-First Search

Now, let’s implement the Breadth-First Search algorithm in Python step by step:

Step 1: Representing the Graph

Before we start implementing BFS, we need to represent the graph in Python. One common approach is to use an adjacency list, where each vertex maps to a list of its neighboring vertices. We can achieve this using a dictionary, as follows:


# Create an adjacency list representation of the graph
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F', 'G'],
    'D': ['B'],
    'E': ['B'],
    'F': ['C'],
    'G': ['C']
}

Step 2: Implementing Breadth-First Search

Now, let’s implement the BFS algorithm using a queue data structure:


from collections import deque

def bfs(graph, start):
    # Create a queue for BFS and a set to keep track of visited vertices
    queue = deque()
    visited = set()

    # Enqueue the start vertex and mark it as visited
    queue.append(start)
    visited.add(start)

    while queue:
        # Dequeue a vertex from the queue
        current_vertex = queue.popleft()
        print(current_vertex, end=' ')

        # Explore its neighbors and enqueue any unvisited neighbors
        for neighbor in graph[current_vertex]:
            if neighbor not in visited:
                queue.append(neighbor)
                visited.add(neighbor)

Step 3: Test the Algorithm

Now that we have implemented the BFS algorithm let’s test it with the graph we defined earlier:


if __name__ == '__main__':
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'D', 'E'],
        'C': ['A', 'F', 'G'],
        'D': ['B'],
        'E': ['B'],
        'F': ['C'],
        'G': ['C']
    }
    
    print("Breadth-First Traversal starting from vertex 'A':")
    bfs(graph, 'A')

When you run the above code, it will produce the following output:


Breadth-First Traversal starting from vertex 'A':
A B C D E F G

Complexity Analysis of Breath-First-Search

Complexity analysis of algorithms helps us understand how their performance scales with input size. Let’s analyze the time complexity and space complexity of the Breadth-First Search (BFS) algorithm:

Time Complexity

In BFS, each vertex and its edges are visited exactly once. The time complexity of BFS depends on the number of vertices (n) and the number of edges (m) in the graph.

  • Enqueuing and dequeuing a vertex from the queue takes constant time O(1).
  • Visiting each vertex’s neighbors requires constant time for each neighbor since we process each edge once.

Therefore, the overall time complexity of BFS is O(n + m), where n is the number of vertices, and m is the number of edges in the graph.

In most cases, the number of edges (m) is proportional to the number of vertices (n) in the graph, giving us a simpler representation of the time complexity as O(n).

Space Complexity

The space complexity of BFS is determined by the additional data structures used during the algorithm’s execution.

  • The queue data structure stores the vertices to be visited. In the worst case, all vertices can be in the queue at once. Therefore, the space complexity due to the queue is O(n).
  • The ‘visited’ set keeps track of the visited vertices. In the worst case, we may have to mark all vertices as visited, resulting in an additional space complexity of O(n) for the ‘visited’ set.
  • The adjacency list representation of the graph requires O(n + m) space, considering both vertices and edges.

Therefore, the overall space complexity of BFS is O(n + m). However, in most cases, the number of edges (m) is proportional to the number of vertices (n), so we can simplify the space complexity as O(n).

It’s essential to consider both time and space complexity when using the BFS algorithm, especially for large graphs, as they can have a significant impact on performance and memory usage. Additionally, for sparse graphs where the number of edges is significantly smaller than the number of vertices, the space complexity may be closer to O(n) rather than O(n + m).

Applications of Breadth-First Search

BFS has a wide range of applications in different domains due to its efficient way of exploring graphs. Here are some common applications of BFS:

1. Shortest Path in Unweighted Graphs

BFS can find the shortest path between two nodes in an unweighted graph. Since BFS visits nodes layer by layer, the first occurrence of the target node will yield the shortest path.

2. Minimum Spanning Tree (Prim’s Algorithm)

BFS can be used as a part of Prim’s algorithm to find the Minimum Spanning Tree (MST) of a connected, undirected graph with weighted edges.

3. Web Crawling

BFS is used in web crawling to index web pages or discover new pages by exploring links starting from a given seed URL.

4. Social Networking

BFS is used to find the shortest path between two users or discover friends within a certain distance in social networks.

5. Garbage Collection

BFS is used in garbage collection algorithms to find and free memory that is no longer reachable from the root objects.

6. Finding Connected Components

BFS can be used to find connected components in an undirected graph. Connected components are subgraphs where all nodes are connected to each other through paths.

7. Serializing/Deserializing Binary Trees

BFS can be used to serialize a binary tree into a string representation and deserialize it back to the original binary tree structure.

8. Finding the Diameter of a Tree

BFS can be used to find the diameter (longest path) of a tree, which represents the longest distance between any two nodes.

9. Finding All Nodes at a Given Distance

BFS can find all nodes at a given distance (k) from a starting node in an unweighted graph.

10. Solving Sliding Puzzle Games

BFS can be used to solve sliding puzzle games, like the 15-puzzle, where the goal is to arrange the tiles in a specific order by sliding them into the empty space.

Conclusion

Breadth-First Search is a fundamental graph traversal algorithm that allows us to explore graphs systematically and find the shortest path from a source vertex to all other vertices in an unweighted graph. By using a queue data structure, BFS ensures that vertices are visited level by level, making it a powerful tool in various applications, including network analysis, shortest path problems, and more. Understanding BFS and its Python implementation opens up a world of possibilities in solving graph-related problems efficiently.

Share your love
Subhankar Rakshit
Subhankar Rakshit

Hey there! I’m Subhankar Rakshit, the brains behind PySeek. I’m a Post Graduate in Computer Science. PySeek is where I channel my love for Python programming and share it with the world through engaging and informative blogs.

Articles: 147