BFS Explained: Shortest Paths in Unweighted Graphs

BFS (Breadth-First Search) is a fundamental graph traversal algorithm that explores a graph or tree level by level—starting from a given source node, it first visits all the neighbor nodes at the current depth (level), then moves on to the nodes at the next depth level. BFS uses a queue data structure to keep track of the nodes to be explored next, ensuring that nodes are processed in the order they are discovered (FIFO: First-In-First-Out).

Unlike DFS (Depth-First Search), which explores as far as possible along a branch before backtracking, BFS guarantees the shortest path (in terms of the number of edges) from the source node to any reachable node in an unweighted graph. It is widely used in pathfinding, network traversal, and connectivity problems.


Core Principles of BFS

BFS follows these key steps for traversing a graph (or tree):

  1. Initialize: Mark the source node as visited (to avoid cycles) and enqueue it into a queue.
  2. Process the Queue: While the queue is not empty:
    • Dequeue the front node (current node) and process it (e.g., print it, store it in a traversal list).
    • Enqueue Neighbors: For each unvisited neighbor of the current node, mark it as visited and enqueue it.
  3. Terminate: The algorithm ends when the queue is empty (all reachable nodes are processed).

Key Data Structures

  • Queue: Manages the order of node exploration (FIFO). In Python, this is typically implemented with collections.deque for efficient enqueue/dequeue operations.
  • Visited Set/Array: Tracks visited nodes to prevent revisiting (critical for graphs with cycles; optional for trees, as they have no cycles).

BFS Implementations

BFS can be applied to adjacency list (the most common graph representation) and tree structures. Below are Python implementations for both.

1. BFS for Graphs (Adjacency List)

Graphs are often represented as adjacency lists (a dictionary where each key is a node, and the value is a list of adjacent nodes). This example traverses an undirected graph:

python

运行

from collections import deque

def bfs_graph(graph, start_node):
    # Initialize visited set and queue
    visited = set()
    queue = deque([start_node])
    visited.add(start_node)
    traversal_order = []  # Store the order of traversal
    
    while queue:
        # Dequeue the front node
        current_node = queue.popleft()
        traversal_order.append(current_node)
        
        # Enqueue all unvisited neighbors
        for neighbor in graph[current_node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    
    return traversal_order

# Test the function with an undirected graph
if __name__ == "__main__":
    # Adjacency list representation of the graph
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'D', 'E'],
        'C': ['A', 'F'],
        'D': ['B'],
        'E': ['B', 'F'],
        'F': ['C', 'E']
    }
    
    start = 'A'
    bfs_result = bfs_graph(graph, start)
    print(f"BFS Traversal starting from {start}: {bfs_result}")
    # Output: ['A', 'B', 'C', 'D', 'E', 'F']

2. BFS for Trees (Binary Tree Example)

Trees are a special case of graphs with no cycles, so the visited set is unnecessary (we simply traverse left/right children):

python

运行

from collections import deque

# Define a binary tree node
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def bfs_tree(root):
    if not root:
        return []  # Return empty list if tree is empty
    
    queue = deque([root])
    traversal_order = []
    
    while queue:
        current_node = queue.popleft()
        traversal_order.append(current_node.val)
        
        # Enqueue left child first (for level-order traversal)
        if current_node.left:
            queue.append(current_node.left)
        if current_node.right:
            queue.append(current_node.right)
    
    return traversal_order

# Test the function with a binary tree
if __name__ == "__main__":
    # Build a sample binary tree:
    #       1
    #      / \
    #     2   3
    #    / \   \
    #   4   5   6
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    root.right.right = TreeNode(6)
    
    bfs_tree_result = bfs_tree(root)
    print(f"BFS Tree Traversal (Level-Order): {bfs_tree_result}")
    # Output: [1, 2, 3, 4, 5, 6]

3. BFS for Shortest Path in Unweighted Graphs

BFS’s level-order traversal makes it ideal for finding the shortest path (number of edges) between two nodes in an unweighted graph:

python

运行

from collections import deque

def bfs_shortest_path(graph, start, target):
    if start == target:
        return [start]  # Shortest path is the node itself
    
    visited = set([start])
    queue = deque([(start, [start])])  # Queue stores (current node, path to node)
    
    while queue:
        current_node, path = queue.popleft()
        
        # Explore all neighbors
        for neighbor in graph[current_node]:
            if neighbor not in visited:
                new_path = path + [neighbor]
                # Check if neighbor is the target
                if neighbor == target:
                    return new_path
                visited.add(neighbor)
                queue.append((neighbor, new_path))
    
    return None  # No path exists between start and target

# Test the shortest path function
if __name__ == "__main__":
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'D', 'E'],
        'C': ['A', 'F'],
        'D': ['B'],
        'E': ['B', 'F'],
        'F': ['C', 'E']
    }
    
    start = 'A'
    target = 'F'
    shortest_path = bfs_shortest_path(graph, start, target)
    if shortest_path:
        print(f"Shortest path from {start} to {target}: {' -> '.join(shortest_path)}")
        # Output: A -> C -> F (or A -> B -> E -> F; both have 2 edges)
    else:
        print(f"No path found from {start} to {target}")


Time and Space Complexity

MetricValueExplanation
Time ComplexityO(V + E)V = number of vertices (nodes), E = number of edges. Each node and edge is processed exactly once.
Space ComplexityO(V)The queue can hold up to V nodes (in the worst case, e.g., a complete binary tree where the last level has ~V/2 nodes). The visited set also stores V nodes.

Notes on Complexity:

  • For treesE = V - 1, so the time complexity simplifies to O(V).
  • For dense graphs (E ≈ V²), BFS is still O(V + E) but may be slower than algorithms optimized for dense graphs (e.g., matrix-based traversal).

Pros and Cons of BFS

Pros

  1. Shortest Path in Unweighted Graphs: Guarantees the shortest path (minimum number of edges) from the source to any reachable node—this is BFS’s most important advantage over DFS.
  2. Level-Order Traversal: Naturally explores nodes level by level, making it ideal for tasks like finding the level of a node or the number of levels in a tree.
  3. No Recursion Overhead: Implemented iteratively with a queue (unlike recursive DFS, which may hit stack overflow for deep graphs/trees).
  4. Complete Traversal: Finds all reachable nodes from the source, making it useful for connectivity checks (e.g., is a graph connected?).

Cons

  1. High Space Complexity: Requires storing all nodes in the queue (O(V) space), which is problematic for very large graphs (e.g., social networks with billions of nodes).
  2. Not Ideal for Deep Graphs: For graphs where the target node is deep, BFS may process many unnecessary nodes at lower levels before reaching the target (DFS is more efficient here).
  3. No Use for Weighted Graphs: Cannot find the shortest path in weighted graphs (use Dijkstra’s algorithm or Bellman-Ford instead).

Real-World Applications of BFS

  1. Shortest Path Finding: GPS navigation (unweighted road networks), finding the shortest path in a maze, or social network friend suggestions (e.g., “Find friends within 3 degrees of separation”).
  2. Web Crawling: Search engines use BFS to crawl web pages—starting from a seed page, they follow links level by level to index new pages.
  3. Network Routing: Protocols like BGP (Border Gateway Protocol) use BFS-like logic to find the shortest path between routers in a network.
  4. Tree Operations: Level-order traversal of trees (e.g., printing a tree level by level, finding the maximum depth of a binary tree).
  5. Connectivity Checks: Determining if a graph is connected (all nodes are reachable from the source) or finding connected components in a disjoint graph.
  6. Cycle Detection: In undirected graphs, BFS can detect cycles by checking if a neighbor is already visited and is not the parent of the current node.

Summary

Best use cases: Shortest path in unweighted graphs, level-order traversal, connectivity checks, and web crawling.

BFS is a level-order graph/tree traversal algorithm that uses a queue to explore nodes.

Key feature: Finds the shortest path in unweighted graphs (minimum number of edges).

Time complexity: O(V + E); Space complexity: O(V).



了解 Ruigu Electronic 的更多信息

订阅后即可通过电子邮件收到最新文章。

Posted in

Leave a comment