Table of Contents
OverviewProblem DefinitionCore Principle: Step-by-Step IllustrationLevel-Order ExpansionDetailed Algorithm StepsTypeScript Implementation1. Generic BFS Template2. Number of Islands3. Open the Lock4. Bidirectional BFSEngineering Optimization: Queues and Access TimingApplications and LimitationsTypical ApplicationsLimitationsSummary
1047 words
5 minutes
Breadth-First Search: BFS Templates and TypeScript in Practice
Overview
Breadth-First Search (BFS) is a traversal algorithm that expands layer by layer. Starting from the source, it first visits nodes at distance 1, then nodes at distance 2, making it naturally suited for finding the minimum number of steps in an unweighted graph.
Prerequisites
- Queue: First in, first out, ensuring layer-by-layer processing
- Graph / grid modeling: Treating states as nodes and single-step transitions as edges
- visited set: Avoiding duplicate enqueues and infinite loops
Problem Definition
Given a starting point and state transition rules, search for the target state in order of increasing distance.
| Element | Description |
|---|---|
| Input | Start point, target, neighbor generation function or graph structure |
| Output | Minimum steps to reach the target, level-order result, or number of connected components |
| Core structure | Queue + visited set |
| Applicability | Every edge has equal weight, or only the minimum number of transitions matters |
Core Principle: Step-by-Step Illustration
BFS expands outward layer by layer like ripples in water:
Level-Order Expansion
- Enqueue the starting point and mark it as visited.
- Each round processes only the nodes currently in the queue, which belong to the same layer.
- Add unvisited neighbors to the queue as the next layer.
- After processing one layer, increment the step count
step + 1.
As soon as the target is encountered for the first time, the current step count can be returned, because BFS guarantees this is the shortest path.
Detailed Algorithm Steps
Algorithm: BFS(start, target)Input: start point start, target targetOutput: minimum number of steps; return -1 if unreachable
1. queue ← [start]2. visited ← {start}3. step ← 04. while queue is not empty:5. size ← queue.length6. repeat size times:7. cur ← queue dequeue8. if cur is target, return step9. iterate over cur's neighbors next:10. if next is unvisited: mark and enqueue11. step ← step + 112. return -1Complexity Analysis:
| Scenario | Time Complexity | Space Complexity | Description |
|---|---|---|---|
| Graph traversal | O(V + E) | O(V) | Each vertex and edge is processed at most once |
| Grid BFS | O(mn) | O(mn) | Each cell is enqueued at most once |
| State search | O(number of states × branching factor) | O(number of states) | Such as combination lock, word ladder |
| Bidirectional BFS | Approximately O(b^(d/2)) | O(b^(d/2)) | Suitable when both start and end are known |
TypeScript Implementation
1. Generic BFS Template
function bfs<T>(start: T, isTarget: (node: T) => boolean, getNeighbors: (node: T) => T[]): number { const queue: T[] = [start]; const visited = new Set<T>([start]); let head = 0; let step = 0;
while (head < queue.length) { const size = queue.length - head;
for (let i = 0; i < size; i++) { const cur = queue[head++]; if (isTarget(cur)) return step;
for (const next of getNeighbors(cur)) { if (visited.has(next)) continue; visited.add(next); queue.push(next); } }
step++; }
return -1;}2. Number of Islands
function numIslands(grid: string[][]): number { const m = grid.length; const n = grid[0].length; const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; let count = 0;
function floodFill(row: number, col: number): void { const queue: [number, number][] = [[row, col]]; let head = 0; grid[row][col] = '0';
while (head < queue.length) { const [x, y] = queue[head++]; for (const [dx, dy] of dirs) { const nx = x + dx; const ny = y + dy; if (nx < 0 || nx >= m || ny < 0 || ny >= n || grid[nx][ny] !== '1') continue; grid[nx][ny] = '0'; queue.push([nx, ny]); } } }
for (let i = 0; i < m; i++) { for (let j = 0; j < n; j++) { if (grid[i][j] === '1') { count++; floodFill(i, j); } } }
return count;}3. Open the Lock
function openLock(deadends: string[], target: string): number { const dead = new Set(deadends); if (dead.has('0000')) return -1;
const queue = ['0000']; const visited = new Set<string>(['0000']); let head = 0; let step = 0;
function nextStates(state: string): string[] { const result: string[] = []; for (let i = 0; i < 4; i++) { const digit = Number(state[i]); for (const next of [(digit + 1) % 10, (digit + 9) % 10]) { result.push(state.slice(0, i) + next + state.slice(i + 1)); } } return result; }
while (head < queue.length) { const size = queue.length - head;
for (let i = 0; i < size; i++) { const cur = queue[head++]; if (cur === target) return step;
for (const next of nextStates(cur)) { if (dead.has(next) || visited.has(next)) continue; visited.add(next); queue.push(next); } }
step++; }
return -1;}4. Bidirectional BFS
function openLockBidirectional(deadends: string[], target: string): number { const dead = new Set(deadends); if (dead.has('0000')) return -1;
let begin = new Set<string>(['0000']); let end = new Set<string>([target]); const visited = new Set<string>(['0000']); let step = 0;
function nextStates(state: string): string[] { const result: string[] = []; for (let i = 0; i < 4; i++) { const digit = Number(state[i]); result.push(state.slice(0, i) + ((digit + 1) % 10) + state.slice(i + 1)); result.push(state.slice(0, i) + ((digit + 9) % 10) + state.slice(i + 1)); } return result; }
while (begin.size > 0 && end.size > 0) { if (begin.size > end.size) [begin, end] = [end, begin];
const nextLayer = new Set<string>(); for (const cur of begin) { if (dead.has(cur)) continue; if (end.has(cur)) return step;
for (const next of nextStates(cur)) { if (visited.has(next)) continue; visited.add(next); nextLayer.add(next); } }
begin = nextLayer; step++; }
return -1;}Engineering Optimization: Queues and Access Timing
JavaScript array shift() shifts subsequent elements; for large queue scenarios, use a head pointer to simulate dequeuing.
| Optimization Point | Recommended Practice | Reason |
|---|---|---|
| Dequeue | const cur = queue[head++] | Avoid the O(n) shifting of shift() |
| Marking visits | Mark when enqueuing | Prevent the same node from being enqueued repeatedly |
| Level counting | Record the current level size | Accurately maintain the shortest step count |
| Bidirectional search | Expand from the smaller frontier | Reduce the exponential state space |
Applications and Limitations
Typical Applications
- Shortest path in unweighted graphs
- Level-order traversal of binary trees
- Grid propagation: islands, mazes, rotting oranges
- State search: combination lock, word ladder
Limitations
| Limitation | Description |
|---|---|
| High memory usage | The queue may hold an entire level of nodes |
| Not suitable for weighted shortest paths | When edge weights differ, algorithms such as Dijkstra should be used |
| State explosion | When the branching factor is large, pruning or bidirectional search is needed |
Summary
Key Points:
- BFS = queue + visited + level-by-level expansion.
- In an unweighted graph, the first time the target is reached is the shortest path.
- For large queues, use a head pointer; do not frequently
shift(). - When both the start and end points are known, prioritize bidirectional BFS.
Breadth-First Search: BFS Templates and TypeScript in Practice
https://www.hehonglei.com/technology/bfs/