Breadth-First Search in Graphs
Problem and examples
Breadth-First Search (BFS) is a fundamental graph traversal algorithm that explores the neighbor nodes level by level. Given a starting node in a graph, BFS visits all its adjacent nodes, then moves on to their neighbors, and so on.
BFS is commonly used for finding the shortest path in unweighted graphs, level order traversal of trees, and in many other scenarios where step-wise discovery is important.
| Input Graph | BFS Traversal | Description |
|---|---|---|
| {0: [1, 2], 1: [0, 2, 3, 4], 2: [0, 1], 3: [1], 4: [1]} | [0, 1, 2, 3, 4] | Starting from node 0, it visits all directly connected nodes (1 and 2), then expands to 1's neighbors (3 and 4). |
| {10: [20, 50], 20: [10, 40], 30: [50], 40: [20, 50], 50: [10, 30, 40]} | [10, 20, 50, 40, 30] | Starting from node 10, BFS visits 20 and 50 first, then explores 50's neighbors (40 and 30). Cycles are avoided. |
| {1: [2, 3], 2: [1, 4], 3: [1, 4], 4: [2, 3, 5], 5: [4]} | [1, 2, 3, 4, 5] | A densely connected graph; BFS explores all layers level-by-level starting from node 1. |
| {1: [2], 2: [1, 3], 3: [2, 4], 4: [3]} | [1, 2, 3, 4] | A linear chain; BFS behaves like DFS here but still visits breadth-first per level. |
| {0: [1], 1: [0], 2: [3], 3: [2]} | [0, 1] | Graph has two disconnected components; BFS from node 0 covers only the first component. |
Explore Breadth-First Search - (BFS) in Graphs
Load an example or enter your own input. Use the player controls to follow each change.
Understanding the Problem
Breadth-First Search (BFS) is a fundamental algorithm used to traverse or search nodes in a graph or tree. The core idea is to explore all nodes at the current depth level before moving on to the next level. BFS uses a queue to keep track of the order in which to visit nodes and ensures each node is visited exactly once using a visited set or array.
Let’s say you’re given a graph as an adjacency list, and you need to traverse all its nodes starting from a given source. The problem becomes even more interesting when you have disconnected graphs or cycles.
Our goal is to understand how BFS works step-by-step and handle these special scenarios properly.
Step-by-Step Solution with Example
step 1: Define the Graph
We will represent the graph using an adjacency list. For example:
const graph = {
A: ['B', 'C'],
B: ['A', 'D'],
C: ['A'],
D: ['B']
};
step 2: Understand the Starting Point
We start BFS from node 'A'. This means we will visit 'A' first and then its neighbors — 'B' and 'C'.
step 3: Use a Queue and a Visited Set
We use a queue to manage the order of visiting nodes, and a set to keep track of visited nodes to avoid revisiting.
const queue = ['A'];
const visited = new Set(['A']);
step 4: Begin the Traversal
We dequeue 'A', visit its neighbors 'B' and 'C', and add them to the queue and the visited set.
while (queue.length > 0) {
const current = queue.shift();
console.log(current); // print visited node
for (const neighbor of graph[current]) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
visited.add(neighbor);
}
}
}
step 5: Track the Order of Visit
In this example, the BFS traversal order will be: A → B → C → D.
step 6: Print or Store the Result
You can print each visited node, or store them in an array for further analysis.
Edge Cases
1. Disconnected Graph
If the graph has multiple disconnected components, starting BFS from one node will only cover its connected component. To visit all nodes, run BFS for each unvisited node:
for (const node in graph) {
if (!visited.has(node)) {
bfs(node);
}
}
2. Graph with Cycles
Cycles can cause infinite loops if visited nodes are not tracked. Always use a visited set to mark nodes that have already been added to the queue.
3. Self-loops
A node may have an edge to itself (e.g., A: ['A']). The visited set will ensure it’s not processed again and again.
4. Empty Graph
If the graph is empty, return an empty traversal list. Always check for this condition before starting BFS.
Finally
BFS is a powerful and intuitive graph traversal algorithm especially useful for problems involving levels, shortest paths in unweighted graphs, or connectivity checks. By thinking of BFS like spreading a message — where each person tells all their immediate friends first — you can intuitively understand how it works. With careful use of a queue and visited tracking, you can avoid pitfalls like cycles or missed components in disconnected graphs.
Always test your implementation against edge cases like cycles, disconnected graphs, and empty inputs to ensure robustness.
- Initialize a queue and a visited set.
- Enqueue the starting node and mark it as visited.
- While the queue is not empty:
- Dequeue the front node.
- Process or record the node.
- For each adjacent node of the current node:
- If it is not visited, enqueue it and mark it as visited.
- Repeat until the queue is empty.
Implementation and efficiency
C implementation
#include <stdio.h>
#include <stdbool.h>
#define MAX 100
int queue[MAX], front = 0, rear = 0;
bool visited[MAX];
void enqueue(int v) {
queue[rear++] = v;
}
int dequeue() {
return queue[front++];
}
bool isEmpty() {
return front == rear;
}
void bfs(int graph[MAX][MAX], int vertices, int start) {
for (int i = 0; i < vertices; i++) visited[i] = false;
enqueue(start);
visited[start] = true;
while (!isEmpty()) {
int node = dequeue();
printf("%d ", node);
for (int i = 0; i < vertices; i++) {
if (graph[node][i] && !visited[i]) {
enqueue(i);
visited[i] = true;
}
}
}
}
int main() {
int graph[MAX][MAX] = {
{0, 1, 1, 0},
{1, 0, 0, 1},
{1, 0, 0, 0},
{0, 1, 0, 0}
};
printf("BFS from node 0: ");
bfs(graph, 4, 0);
return 0;
}C++ implementation
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
void bfs(const vector<vector<int>>& graph, int start) {
vector<bool> visited(graph.size(), false);
queue<int> q;
visited[start] = true;
q.push(start);
while (!q.empty()) {
int node = q.front();
q.pop();
cout << node << " ";
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
}
int main() {
vector<vector<int>> graph = {
{1, 2},
{0, 3},
{0},
{1}
};
cout << "BFS from node 0: ";
bfs(graph, 0);
return 0;
}Python implementation
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
result = []
visited.add(start)
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return result
graph = {
0: [1, 2],
1: [0, 3],
2: [0],
3: [1]
}
print("BFS from node 0:", bfs(graph, 0))Java implementation
import java.util.*;
public class BFSGraph {
public static void bfs(Map<Integer, List<Integer>> graph, int start) {
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
visited.add(start);
queue.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
System.out.print(node + " ");
for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.add(neighbor);
}
}
}
}
public static void main(String[] args) {
Map<Integer, List<Integer>> graph = new HashMap<>();
graph.put(0, Arrays.asList(1, 2));
graph.put(1, Arrays.asList(0, 3));
graph.put(2, Arrays.asList(0));
graph.put(3, Arrays.asList(1));
System.out.print("BFS from node 0: ");
bfs(graph, 0);
}
}JavaScript implementation
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
const result = [];
visited.add(start);
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
const graph = {
0: [1, 2],
1: [0, 3],
2: [0],
3: [1]
};
console.log("BFS from node 0:", bfs(graph, 0));Go implementation
package main
import (
"fmt"
)
func bfs(graph map[int][]int, start int) []int {
visited := make(map[int]bool)
queue := []int{start}
result := []int{}
visited[start] = true
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
for _, neighbor := range graph[node] {
if !visited[neighbor] {
visited[neighbor] = true
queue = append(queue, neighbor)
}
}
}
return result
}
func main() {
graph := map[int][]int{
0: {1, 2},
1: {0, 3},
2: {0},
3: {1},
}
fmt.Println("BFS from node 0:", bfs(graph, 0))
}Rust implementation
use std::collections::{HashMap, HashSet, VecDeque};
fn bfs(graph: &HashMap<i32, Vec<i32>>, start: i32) -> Vec<i32> {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
let mut result = vec![];
visited.insert(start);
queue.push_back(start);
while let Some(node) = queue.pop_front() {
result.push(node);
if let Some(neighbors) = graph.get(&node) {
for &neighbor in neighbors {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
}
result
}
fn main() {
let mut graph = HashMap::new();
graph.insert(0, vec![1, 2]);
graph.insert(1, vec![0, 3]);
graph.insert(2, vec![0]);
graph.insert(3, vec![1]);
let result = bfs(&graph, 0);
println!("BFS from node 0: {:?}", result);
}Kotlin implementation
fun bfs(graph: Map<Int, List<Int>>, start: Int): List<Int> {
val visited = mutableSetOf<Int>()
val queue = ArrayDeque<Int>()
val result = mutableListOf<Int>()
visited.add(start)
queue.add(start)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
result.add(node)
for (neighbor in graph[node] ?: emptyList()) {
if (neighbor !in visited) {
visited.add(neighbor)
queue.add(neighbor)
}
}
}
return result
}
fun main() {
val graph = mapOf(
0 to listOf(1, 2),
1 to listOf(0, 3),
2 to listOf(0),
3 to listOf(1)
)
println("BFS from node 0: ${bfs(graph, 0)}")
}Swift implementation
func bfs(graph: [Int: [Int]], start: Int) -> [Int] {
var visited: Set<Int> = []
var queue: [Int] = [start]
var result: [Int] = []
visited.insert(start)
while !queue.isEmpty {
let node = queue.removeFirst()
result.append(node)
for neighbor in graph[node] ?? [] {
if !visited.contains(neighbor) {
visited.insert(neighbor)
queue.append(neighbor)
}
}
}
return result
}
let graph = [
0: [1, 2],
1: [0, 3],
2: [0],
3: [1]
]
print("BFS from node 0:", bfs(graph: graph, start: 0))TypeScript implementation
function bfs(graph: Record<number, number[]>, start: number): number[] {
const visited = new Set<number>();
const queue: number[] = [start];
const result: number[] = [];
visited.add(start);
while (queue.length > 0) {
const node = queue.shift()!;
result.push(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
const graph: Record<number, number[]> = {
0: [1, 2],
1: [0, 3],
2: [0],
3: [1],
};
console.log("BFS from node 0:", bfs(graph, 0));Time complexity
| Case | Time | Explanation |
|---|---|---|
| best case | O(V + E) | Even in the best case, BFS must explore every vertex (V) and every edge (E) to ensure complete traversal of the graph component. |
| average case | O(V + E) | On average, BFS visits all vertices and edges once during the traversal. |
| worst case | O(V + E) | In the worst case, such as a fully connected graph, BFS still processes all vertices and edges exactly once. |
Space complexity
O(V)
The space complexity is proportional to the number of vertices because the queue and visited set each store up to V elements.