BFS Fundamentals and Implementation
How BFS Works
BFS explores vertices level by level, visiting all neighbors at the current depth before moving deeper. It uses a queue (FIFO) to maintain the frontier.
Graph: 0
/ \
1 2
/ \\ \
3 4 5
BFS Order: 0 → 1 → 2 → 3 → 4 → 5
Level 0: [0]
Level 1: [1, 2]
Level 2: [3, 4, 5]
Core Algorithm
import java.util.*;
public class BFS {
// Standard BFS traversal
public List<Integer> bfs(Map<Integer, List<Integer>> graph, int start) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
result.add(node);
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
return result;
}
// BFS with level tracking
public List<List<Integer>> bfsByLevel(Map<Integer, List<Integer>> graph, int start) {
List<List<Integer>> levels = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
int node = queue.poll();
currentLevel.add(node);
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
levels.add(currentLevel);
}
return levels;
}
}
BFS Template for Interview Problems
// Complete BFS template with distance tracking
public int bfsShortestPath(Map<Integer, List<Integer>> graph, int start, int target) {
if (start == target) return 0;
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
Map<Integer, Integer> distance = new HashMap<>();
queue.offer(start);
visited.add(start);
distance.put(start, 0);
while (!queue.isEmpty()) {
int node = queue.poll();
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
distance.put(neighbor, distance.get(node) + 1);
queue.offer(neighbor);
if (neighbor == target) {
return distance.get(neighbor);
}
}
}
}
return -1; // Target not reachable
}
Complexity Analysis
| Operation | Time | Space |
|---|---|---|
| BFS Traversal | O(V + E) | O(V) |
| Shortest Path (unweighted) | O(V + E) | O(V) |
| Level-by-Level | O(V + E) | O(V) |
Key Properties
- Shortest Path: BFS guarantees shortest path in unweighted graphs
- Complete: BFS will find a solution if one exists
- Optimal: Among all algorithms with the same completeness, BFS finds the shallowest goal
Advanced BFS Patterns
Multi-Source BFS
When you need to find distances from multiple starting points simultaneously.
// Example: Rotting Oranges - multi-source BFS
public int orangesRotting(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
// Add all rotten oranges as sources
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) {
queue.offer(new int[]{r, c});
} else if (grid[r][c] == 1) {
fresh++;
}
}
}
if (fresh == 0) return 0;
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
int minutes = 0;
while (!queue.isEmpty()) {
int size = queue.size();
boolean rotted = false;
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
for (int[] dir : dirs) {
int nr = cell[0] + dir[0];
int nc = cell[1] + dir[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
grid[nr][nc] = 2;
fresh--;
rotted = true;
queue.offer(new int[]{nr, nc});
}
}
}
if (rotted) minutes++;
}
return fresh == 0 ? minutes : -1;
}
0-1 BFS (Weighted Graphs with Two Weights)
When edges have only two possible weights (0 or 1), use a deque instead of a queue.
// 0-1 BFS using deque
public int zeroOneBFS(List<int[]>[] graph, int start, int n) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(start);
while (!deque.isEmpty()) {
int node = deque.pollFirst();
for (int[] edge : graph[node]) {
int neighbor = edge[0];
int weight = edge[1];
if (dist[node] + weight < dist[neighbor]) {
dist[neighbor] = dist[node] + weight;
if (weight == 0) {
deque.offerFirst(neighbor);
} else {
deque.offerLast(neighbor);
}
}
}
}
return dist;
}
BFS on Grid
// BFS on 2D grid - common pattern
public int bfsGrid(int[][] grid, int startR, int startC) {
int rows = grid.length, cols = grid[0].length;
boolean[][] visited = new boolean[rows][cols];
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{startR, startC});
visited[startR][startC] = true;
int steps = 0;
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
// Process cell here
if (isTarget(cell[0], cell[1])) return steps;
for (int[] dir : dirs) {
int nr = cell[0] + dir[0];
int nc = cell[1] + dir[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& !visited[nr][nc] && grid[nr][nc] != 0) {
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc});
}
}
}
steps++;
}
return -1;
}
Common BFS Problems
| Problem | Pattern |
|---|---|
| Shortest Path in Unweighted Graph | Standard BFS |
| Word Ladder | BFS on implicit graph |
| Rotting Oranges | Multi-source BFS |
| Open the Lock | BFS on state space |
| Binary Tree Level Order | BFS on tree |
| Knight's Tour | BFS on grid |
Interactive Visualization
Breadth-First Search (BFS) Traversal
Practice Problems
Given two words (beginWord and endWord) and a word dictionary, find the length of the shortest transformation sequence from beginWord to endWord, changing only one letter at a time. Each intermediate word must be in the dictionary.
Example:
Input: beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
hit → hot → dot → dog → cog (5 transformations)
Solution
```java
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
Set<String> visited = new HashSet<>();
visited.add(beginWord);
int level = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String word = queue.poll();
char[] chars = word.toCharArray();
for (int j = 0; j < chars.length; j++) {
char original = chars[j];
for (char c = 'a'; c <= 'z'; c++) {
if (c == original) continue;
chars[j] = c;
String newWord = new String(chars);
if (newWord.equals(endWord)) return level + 1;
if (wordSet.contains(newWord) && !visited.contains(newWord)) {
visited.add(newWord);
queue.offer(newWord);
}
}
chars[j] = original;
}
}
level++;
}
return 0;
}
```Edge Cases:
- beginWord equals endWord (return 0)
- endWord not in wordList (return 0)
- Single character transformation needed
- Large word dictionary - need efficient lookup
In a grid, each cell can be 0 (empty), 1 (fresh orange), or 2 (rotten orange). Every minute, rotten oranges rot adjacent fresh oranges. Return the minimum minutes until no fresh orange remains.
Example:
Input: grid = [[2,1,1],[1,1,0],[0,1,1]]
Output: 4
All oranges rot in 4 minutes.
Solution
```java
public int orangesRotting(int[][] grid) {
Queue<int[]> queue = new LinkedList<>();
int fresh = 0;
for (int i = 0; i < grid.length; i++)
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 2) queue.offer(new int[]{i, j});
if (grid[i][j] == 1) fresh++;
}
if (fresh == 0) return 0;
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
int minutes = 0;
while (!queue.isEmpty()) {
int size = queue.size();
boolean rotted = false;
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
for (int[] d : dirs) {
int nr = cell[0]+d[0], nc = cell[1]+d[1];
if (nr>=0 && nr<grid.length && nc>=0 && nc<grid[0].length && grid[nr][nc]==1) {
grid[nr][nc] = 2;
fresh--;
rotted = true;
queue.offer(new int[]{nr, nc});
}
}
}
if (rotted) minutes++;
}
return fresh == 0 ? minutes : -1;
}
```Edge Cases:
- No fresh oranges
- Some oranges unreachable
- All rotten
You have a lock with 4 circular wheels. Each wheel has digits 0-9. Each turn moves one wheel one step. Return the minimum turns to reach target from 0000.
Example:
Input: deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
Minimum 6 turns to reach target.
Solution
```java
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));
Set<String> visited = new HashSet<>();
if (dead.contains("0000")) return -1;
Queue<String> queue = new LinkedList<>();
queue.offer("0000");
visited.add("0000");
int steps = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String curr = queue.poll();
if (curr.equals(target)) return steps;
for (int j = 0; j < 4; j++) {
for (int d = -1; d <= 1; d += 2) {
char[] arr = curr.toCharArray();
arr[j] = (char) ((arr[j] - '0' + d + 10) % 10 + '0');
String next = new String(arr);
if (!visited.contains(next) && !dead.contains(next)) {
visited.add(next);
queue.offer(next);
}
}
}
}
steps++;
}
return -1;
}
```Edge Cases:
- Target is deadend
- Target is 0000
- All neighbors dead
Quiz
1. Why does BFS guarantee the shortest path in unweighted graphs?
2. In a multi-source BFS, when should you add all sources to the queue initially vs processing them one by one?
3. What is the primary purpose of Breadth-First Search (BFS)?
4. What is a common mistake when implementing Breadth-First Search (BFS)?
Flashcards
Question
What data structure does BFS use and why?
Click to reveal answer
Answer
BFS uses a queue (FIFO) because it processes nodes in the order they were discovered - first in, first out. This ensures level-by-level exploration.
Question
When should you use BFS vs DFS for finding a path?
Click to reveal answer
Answer
Use BFS when you need the shortest path in an unweighted graph. BFS guarantees the shortest path. Use DFS when you just need any path, or when the graph is very deep and BFS would use too much memory.
Question
What is Breadth-First Search (BFS)?
Click to reveal answer
Answer
Breadth-First Search (BFS) is a key concept in software engineering.
Question
When to use Breadth-First Search (BFS)?
Click to reveal answer
Answer
Use Breadth-First Search (BFS) when building production systems that require reliability, scalability, and maintainability.
Question
Breadth-First Search (BFS) best practices
Click to reveal answer
Answer
Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.
Revision Notes
Key Takeaways
- 1.BFS uses a queue and explores level by level
- 2.BFS guarantees shortest path in unweighted graphs
- 3.Track visited nodes to prevent revisiting
- 4.Multi-source BFS: add all sources to queue simultaneously
- 5.For grids, use direction arrays for cleaner neighbor generation
Interview Tips
- •BFS is your go-to for shortest path in unweighted graphs
- •Use level-by-level BFS when you need to process by distance
- •Multi-source BFS is common - practice Rotting Oranges and similar problems
- •For implicit graphs (Word Ladder), define state transitions carefully
- •Remember to mark visited BEFORE adding to queue to avoid duplicates
Cheat Sheet
BFS Cheat Sheet
Standard BFS Template
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
BFS with Distance
Map<Integer, Integer> dist = new HashMap<>();
dist.put(start, 0);
// ... in loop:
dist.put(neighbor, dist.get(node) + 1);
Level-by-Level BFS
while (!queue.isEmpty()) {
int levelSize = queue.size();
for (int i = 0; i < levelSize; i++) {
int node = queue.poll();
// process all nodes at current level
}
}
Multi-Source BFS
- Add ALL sources to queue at start
- Process simultaneously
- Useful: Rotting Oranges, Distance to Nearest 1
Grid BFS Directions
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
// Add bounds checking: nr >= 0 && nr < rows && nc >= 0 && nc < cols
Complexity
- Time: O(V + E)
- Space: O(V)
- Shortest path: O(V + E)