انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-043-L03

البحث بالعرض أولاً (Breadth-first search)

  • الدرس 3 من 6
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

You can now store a graph. The next question is the one every graph program asks: starting at one node, how do we visit the rest in an orderly way, without missing nodes and without going around in circles?

Breadth-first search (BFS) is the first answer. Picture a stone dropped in a pond. The ripple reaches everything one step away first, then everything two steps away, then three. BFS explores a graph the same way, in rings around the starting node. That ring-by-ring order has a valuable side effect: the first time BFS reaches a node, it has used the fewest possible edges. BFS therefore finds shortest paths in graphs where every edge counts the same.

We use a queue, the structure you met in the collections module, and a visited set. All code was compiled and run with JDK 21.

Why this matters: "how many hops away is this?" appears in routing, social networks, and service maps. BFS is the standard way to answer it.

This lesson builds on "Representing graphs" (the Map<String, List<String>> adjacency list), on Queue and ArrayDeque from the collections module, and on HashSet.

Learning Objectives

By the end of this lesson you will be able to:

  • Describe BFS as exploring a graph in rings around a start node.
  • Write BFS in Java with a Queue, a visited Set, and the adjacency list.
  • Trace BFS by hand, showing the queue after each step.
  • Explain why a visited set is required when a graph has cycles.
  • Explain why BFS finds the fewest-edge distance in an unweighted graph.
  • State BFS cost as O(V + E) and say why.
  • Handle unreachable nodes and unknown start nodes.

Written Explanation

The idea: rings around the start

Starting at node S, BFS does this:

  1. Visit S.
  2. Visit all neighbors of S (one edge away).
  3. Visit all neighbors of those nodes that are new (two edges away).
  4. Continue until no new nodes remain.

The data structure that gives this order is a queue: first in, first out. Nodes that are discovered first are processed first, so all nodes one edge away are handled before any node two edges away. The queue always contains a mix of at most two neighboring rings.

The algorithm

java
Queue<String> q = new ArrayDeque<>();
Set<String> visited = new HashSet<>();
q.add(start);
visited.add(start);
while (!q.isEmpty()) {
    String cur = q.poll();
    for (String next : g.getOrDefault(cur, List.of())) {
        if (visited.add(next)) q.add(next);
    }
}

Walk through it in words:

  • Put the start node in the queue and mark it visited.
  • Repeatedly take the node at the front (poll). It is the one we process now.
  • For each neighbor of that node, if we have not seen it before, mark it visited and add it to the back of the queue.

Set.add returns true only when the element was not already in the set. So visited.add(next) does two jobs at once: it asks "is this new?" and records it. The documentation for ArrayDeque says it has no capacity limit and that poll returns null on an empty deque, which is why the loop tests isEmpty() first. It also prohibits null elements, which is fine here because node names are never null.

Why the visited set is not optional

Look at a graph with a loop: A is linked to B and B to A. Without a visited set, processing A adds B, processing B adds A, processing A adds B again, and so on. The queue never empties and the program never ends. This is the module's most common mistake: forgetting the visited set. Every node must be marked the moment it is added to the queue, so it can never be added twice.

Mark visited when you add to the queue, not when you remove. Marking at removal time still gives correct answers if you skip nodes already seen, but a node can sit in the queue several times first, wasting memory.

Why BFS finds shortest paths

Think of the rings. Every node one edge from S is added to the queue before any node two edges away, because nodes two edges away are discovered only while processing nodes one edge away. By induction the queue is always in order of distance: all distance-1 nodes, then distance-2 nodes, and so on. A node is added the first time any neighbor reaches it, and that neighbor came from the earlier ring. So the first discovery is the shortest.

This holds when every edge counts equally, as in "stops on a metro line", which is what "unweighted" means. BFS counts edges, not kilometers or minutes. A standard algorithms reference states it the same way: the path BFS finds to any node is a shortest path in terms of edge count.

To record distances, store them in a map as you discover nodes. The distance of a new node is its discoverer's distance plus one. Using the map's keys as the visited set saves a separate set:

java
Map<String, Integer> dist = new LinkedHashMap<>();
dist.put(start, 0);
q.add(start);
while (!q.isEmpty()) {
    String cur = q.poll();
    for (String next : g.getOrDefault(cur, List.of())) {
        if (!dist.containsKey(next)) {
            dist.put(next, dist.get(cur) + 1);
            q.add(next);
        }
    }
}

LinkedHashMap keeps insertion order, so printing it also shows the visiting order.

Cost: O(V + E)

Each node is added to the queue at most once, thanks to the visited set, so the loop body runs at most V times. Over the whole run, each node's neighbor list is scanned once, and the lists together hold E entries (or 2 E for undirected graphs). The total work is proportional to V + E, written O(V + E). This is the cost for an adjacency list. It is the same figure that standard references give for BFS: linear in the number of nodes plus edges. With a matrix, finding each node's neighbors would cost V per node, giving O(V x V) instead.

Unreachable nodes and unknown starts

BFS visits only the nodes reachable from the start. If the graph has separate pieces, the other pieces are never touched, and their nodes do not appear in visited or dist. That is how BFS tells you "unreachable": the node is simply absent.

If the start node is not in the graph at all, getOrDefault makes the loop harmless but the start node itself still goes into the result. Check g.containsKey(start) first when an unknown name should be an error or an empty answer.

Examples

Example 1: tracing BFS by hand (simple)

Graph (undirected, neighbors in the order shown): A: [B, C]; B: [A, D]; C: [A, D]; D: [B, C, E]; E: [D]. BFS from A:

StepProcessNewly addedQueue afterVisited
0startA[A]A
1AB, C[B, C]A, B, C
2BD (A already seen)[C, D]A, B, C, D
3Cnone (A, D seen)[D]same
4DE[E]A, B, C, D, E
5Enone[]all

Visiting order: A, B, C, D, E. Distances from A: A 0; B 1, C 1; D 2; E 3. D is reached via B first (step 2), and C reaches it later but finds it already seen. D has two shortest routes, A-B-D and A-C-D; BFS records the first it meets.

Example 2: order and distances in code (practical)

java
import java.util.*;

public class L03 {
    static Map<String, List<String>> g = new HashMap<>();
    static void edge(String a, String b) {
        g.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
        g.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
    }

    static List<String> bfsOrder(String start) {
        List<String> order = new ArrayList<>();
        Queue<String> q = new ArrayDeque<>();
        Set<String> visited = new HashSet<>();
        q.add(start);
        visited.add(start);
        while (!q.isEmpty()) {
            String cur = q.poll();
            order.add(cur);
            for (String next : g.getOrDefault(cur, List.of())) {
                if (visited.add(next)) q.add(next);
            }
        }
        return order;
    }

    public static void main(String[] args) {
        edge("A","B"); edge("A","C"); edge("B","D"); edge("C","D"); edge("D","E");
        System.out.println(bfsOrder("A"));
    }
}

Output:

text
[A, B, C, D, E]

It matches the hand trace. Using the dist version above on the same graph, distances("A") printed {A=0, B=1, C=1, D=2, E=3}.

Example 3: separate pieces and an unknown start (realistic)

Add a second piece to the same graph with edge("X","Y"), so X and Y are linked only to each other. Then:

java
System.out.println(bfsOrder("A"));            // [A, B, C, D, E]
System.out.println(distances("X"));           // {X=0, Y=1}
System.out.println(bfsOrder("Q"));            // [Q]

I ran these and got the three outputs shown in the comments. From A, the nodes X and Y never show up: they are unreachable, and BFS reports that by omission. The third line is a trap. Q is not in the graph, yet bfsOrder("Q") returns [Q], because the start goes into the result before any neighbor lookup. A route finder must check g.containsKey("Q") first and answer "unknown station". The lab expects exactly this.

Backend Insight

BFS is the engine behind "how many hops": the number of connections between two people on a social platform, the number of network hops between two machines, the fewest transfers between two stations. In service maps it answers "which services are within two calls of this one", which is useful when you need to know what a failing service might drag down.

Cost matters at scale. O(V + E) is linear, so doubling the graph roughly doubles the work. The memory cost is the queue plus the visited set. In a very wide graph the queue can hold a whole ring of nodes at once, which can be large. Real systems often bound the search, for example "stop after 3 hops", which the distance map makes easy: just do not expand nodes whose distance has reached the limit.

Common Mistakes

Mistake 1: No visited set

On any graph with a cycle, including every undirected edge (A-B lets you go back from B to A), BFS never stops. Always mark nodes before queueing them.

Mistake 2: Marking visited too late

If you mark a node only after taking it from the queue, several neighbors may queue the same node before it is processed. The answer can still be right, but the queue grows needlessly. Mark on add.

Mistake 3: Using a stack instead of a queue

A stack (last in, first out) goes deep first, not ring by ring. The result is no longer BFS, and distances found this way are not shortest. For BFS use Queue with add and poll.

Mistake 4: Assuming BFS finds the cheapest route in weighted graphs

BFS counts edges. If edges have different lengths or costs, the route with the fewest edges may not be the cheapest. Weighted shortest paths need other algorithms, beyond this module.

Mistake 5: Not checking that the start node exists

An unknown start returns a one-element result instead of an error. Check containsKey first.

Exercises

Try each exercise yourself before opening the solutions.

Use this graph for Level A and B1/B2. It is undirected, and neighbors are tried in the order shown: S: [A, B]; A: [S, C]; B: [S, C, D]; C: [A, B, E]; D: [B, E]; E: [C, D].

Level A: Foundation

A1. Write the BFS visiting order starting from S.

أظهر الحل A1

A1. S, A, B, C, D, E. S adds A and B; A adds C; B adds D (S and C are already seen); C adds E.

A2. Write the distance (number of edges) from S to each node.

أظهر الحل A2

A2. S 0; A 1, B 1; C 2, D 2; E 3.

A3. Show the queue contents right after S, then A, then B have been processed.

أظهر الحل A3

A3. After S: [A, B]. After A: [B, C]. After B: [C, D].

Level B: Application

B1. How many nodes are within 2 edges of S, counting S itself? Name them.

أظهر الحل B1

B1. Nodes at distance 0, 1, or 2: S, A, B, C, D. That is 5 nodes. E is at distance 3.

B2. Give one shortest path from S to E and its length. Is it the only one? Why does BFS only need to report one?

أظهر الحل B2

B2. For example S, A, C, E: 3 edges. It is not the only one: S, B, C, E and S, B, D, E also have 3 edges. BFS only needs to report one because the question asks for the fewest edges, and any of them answers it.

B3. On a graph with edges X-Y, Y-Z, Z-X, Z-W, run BFS from X by hand (neighbors in the order Y, Z for X; X, Y, W for Z; X, Z for Y). List the visiting order and explain what stops the loop X-Y-Z-X from repeating.

أظهر الحل B3

B3. Order: X, Y, Z, W. X adds Y and Z. Y finds X and Z already visited. Z finds X and Y visited and adds W. W finds Z visited. The visited set stops the loop: Z is never added again once marked.

Level C: Challenge

C1. Write a Java method countComponents() that returns how many separate pieces the graph has, using BFS. Hint: loop over all keys, and start a new BFS whenever you meet a node that is not yet visited.

أظهر الحل C1

C1.

java
static int countComponents() {
    Set<String> visited = new HashSet<>();
    int count = 0;
    for (String start : g.keySet()) {
        if (visited.contains(start)) continue;
        count++;
        Queue<String> q = new ArrayDeque<>();
        q.add(start);
        visited.add(start);
        while (!q.isEmpty()) {
            String cur = q.poll();
            for (String next : g.getOrDefault(cur, List.of())) {
                if (visited.add(next)) q.add(next);
            }
        }
    }
    return count;
}

I ran it on edges A-B, B-C, D-E plus a lone node F (created with computeIfAbsent("F", ...)); it printed 3.

C2. A teammate marks nodes visited only when they are removed from the queue, skipping any removed node that was already visited. Is the BFS order still correct? What is the cost?

أظهر الحل C2

C2. The visiting order is still correct, because the first time a node is removed is still in ring order. Memory use is worse: a node can be added to the queue once for each edge that points at it, so the queue can hold up to about E entries instead of V. The time stays O(V + E), but marking on add is leaner and clearer.

Mini Challenge

Take the five-node graph from Example 1 and add two more nodes, F linked to E and G linked to F. Without running code, predict the BFS order from A and the distance to G. Then write the program, run it, and compare.

Extend it in two ways:

  1. Make distances stop expanding nodes at distance 2, and print what it finds.
  2. Add an unreachable node (for example edge("P","Q")) and print whether distances("A") contains "P".

Check your work: your prediction matched the output (G should be 5 edges from A), the limited search leaves farther nodes out, and the unreachable node is absent from the map.

Summary & Next Lesson

Key takeaways

  • BFS explores a graph in rings: the start, then everything one edge away, then two, and so on.
  • It uses a queue (add and poll) and a visited set. Mark a node when you add it to the queue.
  • Without a visited set, any cycle makes BFS loop forever.
  • In an unweighted graph, the first time BFS reaches a node it has used the fewest edges, so BFS finds shortest paths. It counts edges, not weights.
  • Cost is O(V + E) with an adjacency list.
  • Unreachable nodes are never visited; an unknown start node must be checked separately.

Next lesson

The next lesson, "Depth-first search", explores the same graph the opposite way: follow one path as deep as it goes, then back up. It also shows when to choose DFS and when to choose BFS.

Self-check

  1. Which structure gives BFS its ring-by-ring order?
  2. When should a node be marked visited?
  3. What happens on a graph with a cycle if there is no visited set?
  4. Why does BFS find the fewest-edge path in an unweighted graph?
  5. What is BFS's cost for V nodes and E edges?
أظهر الإجابات
  1. A queue.
  2. When it is added to the queue.
  3. The same nodes are added again and again, so the search never finishes.
  4. Nodes are processed in order of distance, so the first time a node is reached is by the fewest edges.
  5. O(V + E).

References

  • Oracle, ArrayDeque (Java SE 21 API). Supports that ArrayDeque has no capacity limit, prohibits null elements, returns null from poll when empty, and is likely faster than LinkedList as a queue.
  • CP-Algorithms, Breadth First Search. Supports that BFS finds shortest paths (fewest edges) in an unweighted graph, runs in O(n + m) with a queue, and can record distances and parents.
  • The ring-by-ring argument for why BFS is shortest is a simplified explanation written for this course, not a formal proof. I did not separately verify the claim about marking at removal time beyond reasoning and the hand trace. All Java code was compiled and run with JDK 21.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.