跳转至

21 Breadth-First Search

In the previous chapter, you explored using graphs to capture relationships between objects. Remember that objects are just vertices, and edges represent the relationships between them.

Several algorithms exist to traverse or search through a graph’s vertices. One such algorithm is the breadth-first search (BFS) algorithm. The BFS algorithm visits the closest vertices from the starting vertex before moving on to further vertices.

BFS can be used to solve a wide variety of problems:

  1. Generating a minimum-spanning tree.
  2. Finding potential paths between vertices.
  3. Finding the shortest path between two vertices.

How Breadth-First Search Works

A breadth-first search starts by selecting any vertex in a graph. The algorithm then explores all neighbors of this vertex before traversing the neighbors’ neighbors and so forth.

You’ll use the following undirected graph as an example to explore how BFS works:

DCHGFEBA

A queue will help you keep track of which vertices to visit next. The first-in-first-out approach of the queue guarantees that all of a vertex’s neighbors are visited before you traverse one level deeper.

To begin, you pick a source vertex to start from, in this case, A. Then add it to the queue. Highlighted vertices will represent vertices that you’ve already visited:

DCHGFEBAQueueA

Next, you dequeue the A and add all of its neighboring vertices [B, D, C] to the queue:

HGFEABDCQueueABCD

It’s important to note that you only add a vertex to the queue when it has not yet been visited and is not already in the queue.

The queue isn’t empty, so you dequeue and visit the next vertex, B. You then add B’s neighbor E to the queue. A has already been visited, so it doesn’t get added. The queue now has [D, C, E]:

BADCEQueueHGFABDCE

The next vertex to be dequeued is D. D doesn’t have any neighbors that haven’t been visited. The queue now has [C, E]:

ABDCEQueueHGFABDCE

Next, you dequeue C and add its neighbors [F, G] to the queue. The queue now has [E, F, G]:

ABDCEFGQueueHABDCEFG

You’ve visited all of A’s neighbors! BFS now moves on to the second level of neighbors. You dequeue E and add H to the queue. The queue now has [F, G, H]. You don’t add B or F to the queue because B has already been visited and F is already in the queue:

ABDCEFGHQueueHABDCEFG

You dequeue F, and since all its neighbors are already in the queue or visited, you don’t add anything to the queue:

ABDECFGHQueueABDCGEFH

Like the previous step, you dequeue G and don’t add anything to the queue:

ABDECFGHQueueABDCEFGH

Finally, you dequeue H. The breadth-first search is complete since the queue is now empty!

ABDECFGHQueueABDCEHFG

When exploring the vertices, you can construct a tree-like structure, showing the vertices at each level: first the vertex you started from, then its neighbors, then its neighbors’ neighbors and so on.

ABDCHEFG

Implementation

Open up the starter project for this chapter. The lib folder contains the graph implementation you built in the previous chapter. It also includes the stack-based queue implementation that you made in Chapter 6, “Queues”. You’ll use both of these files to create your breadth-first search.

Extension on Graph

Rather than directly modifying Graph, you’ll add an extension to it. Create a new file in lib named breadth_first_search.dart. Then add the following code to that file:

import 'queue.dart';
import 'graph.dart';

extension BreadthFirstSearch<E> on Graph<E> {
  List<Vertex<E>> breadthFirstSearch(Vertex<E> source) {
    final queue = QueueStack<Vertex<E>>();
    Set<Vertex<E>> enqueued = {};
    List<Vertex<E>> visited = [];

    // more to come

    return visited;
  }
}

You’ve defined an extension method on Graph called breadthFirstSearch, which takes in a starting vertex. The method uses three data structures:

  1. queue keeps track of the neighboring vertices to visit next.
  2. enqueued remembers which vertices have been enqueued before, so you don’t enqueue the same vertex twice. You use Set so that lookup is cheap and only takes O(1) time. A list would require O(n) time.
  3. visited is a list that stores the order in which the vertices were explored.

Next, complete the method by replacing the // more to come comment with the following:

// 1
queue.enqueue(source);
enqueued.add(source);

while (true) {
  // 2
  final vertex = queue.dequeue();
  if (vertex == null) break;
  // 3
  visited.add(vertex);
  // 4
  final neighborEdges = edges(vertex);
  for (final edge in neighborEdges) {
    // 5
    if (!enqueued.contains(edge.destination)) {
      queue.enqueue(edge.destination);
      enqueued.add(edge.destination);
    }
  }
}

Here’s what’s going on:

  1. You initialize the BFS algorithm by first enqueuing the source vertex.
  2. You continue to dequeue a vertex from the queue until the queue is empty.
  3. Every time you dequeue a vertex from the queue, you add it to the list of visited vertices.
  4. Then, you find all edges that start from the current vertex and iterate over them.
  5. For each edge, you check to see if its destination vertex has been enqueued before, and, if not, you add it to the queue.

Testing it Out

That’s all there is to implementing BFS! Give this algorithm a spin. Open bin/starter.dart and replace the contents of the file with the following code:

import 'package:starter/breadth_first_search.dart';
import 'package:starter/graph.dart';

void main() {
  final graph = AdjacencyList<String>();

  final a = graph.createVertex('A');
  final b = graph.createVertex('B');
  final c = graph.createVertex('C');
  final d = graph.createVertex('D');
  final e = graph.createVertex('E');
  final f = graph.createVertex('F');
  final g = graph.createVertex('G');
  final h = graph.createVertex('H');

  graph.addEdge(a, b, weight: 1);
  graph.addEdge(a, c, weight: 1);
  graph.addEdge(a, d, weight: 1);
  graph.addEdge(b, e, weight: 1);
  graph.addEdge(c, f, weight: 1);
  graph.addEdge(c, g, weight: 1);
  graph.addEdge(e, h, weight: 1);
  graph.addEdge(e, f, weight: 1);
  graph.addEdge(f, g, weight: 1);

  // more to come
}

This creates the same graph you saw earlier:

DCHGFEBA

Now add the following code at the bottom of main:

final vertices = graph.breadthFirstSearch(a);
vertices.forEach(print);

Run that and note the order that BFS explored the vertices in:

A
B
C
D
E
F
G
H

One thing to keep in mind with neighboring vertices is that the order in which you visit them is determined by how you construct your graph. You could have added an edge between A and C before adding one between A and B. In that case, the output would list C before B.

Performance

When traversing a graph using BFS, each vertex is enqueued once. This process has a time complexity of O(V). During this traversal, you also visit all the edges. The time it takes to visit all edges is O(E). Adding the two together means that the overall time complexity for breadth-first search is O(V + E).

The space complexity of BFS is O(V) since you have to store the vertices in three separate structures: queue, enqueued and visited.

Challenges

Ready to try a few challenges? If you get stuck, the answers are in the Challenge Solutions section and also in the supplemental materials that accompany this book.

Challenge 1: Maximum Queue Size

For the following undirected graph, list the maximum number of items ever in the queue. Assume that the starting vertex is A.

ACBDEFIJHG

Challenge 2: Iterative BFS

In this chapter, you create an iterative implementation of breadth-first search. Now write a recursive solution.

Challenge 3: Disconnected Graph

Add a method to Graph to detect if a graph is disconnected. An example of a disconnected graph is shown below:

ACBDEFHG

Key Points

  • Breadth-first search (BFS) is an algorithm for traversing or searching a graph.
  • BFS explores all the current vertex’s neighbors before traversing the next level of vertices.
  • It’s generally good to use this algorithm when your graph structure has many neighboring vertices or when you need to find out every possible outcome.
  • The queue data structure is used to prioritize traversing a vertex’s edges before diving down to a level deeper.