作为一名既写前端业务、又啃算法题、还爱碎碎念的博主,今天必须跟大家聊聊 BFS 算法------ 这个看似基础却能解决无数实际问题的 "万能钥匙"。
你可能会说:"前端工程师学 BFS 有必要吗?" 其实在前端开发中,BFS 的身影无处不在:从 DOM 树的逐层遍历,到前端路由的权限校验,再到可视化图表的节点渲染,甚至是小游戏里的 AI 路径规划...... 掌握 BFS,能让你在解决复杂问题时多一种 "降维打击" 的思路。
一、BFS 算法初相识

(一)什么是 BFS 算法?
BFS 全称 Breadth-First Search(广度优先搜索) ,顾名思义,它是一种 "先宽后深" 的搜索方式。
可以用一个生活场景类比:假设你站在一片水域的岸边,往水里扔了一块石头 ------ 水波会从落点开始,逐层向外扩散,先覆盖最近的水域,再慢慢波及远处。BFS 的搜索过程就像这水波,从起点开始,先访问所有 "邻居节点",再访问邻居的邻居,直到遍历完所有可达节点。
再举个前端相关的例子:如果把 DOM 树看作一个节点网络,document.querySelector
的查找过程其实就类似 BFS------ 先检查当前节点的直接子元素(第一层),如果没找到再检查子元素的子元素(第二层),直到找到目标元素。
(二)BFS 算法的核心思想
BFS 的核心实现依赖 队列(Queue) 这种数据结构(先进先出,FIFO),步骤可以概括为:
-
初始化队列:将起始节点放入队列,并标记为 "已访问"(避免重复访问);
-
循环处理队列:当队列不为空时,取出队头节点;
-
访问并扩散:访问当前节点,然后将它所有未访问的邻居节点放入队列末尾;
-
重复步骤 2-3:直到队列为空,所有可达节点均被访问。
用一句话总结: "出队一个节点,入队它的所有未访问邻居" 。
(三)BFS 算法的应用场景
BFS 特别适合解决 "最短路径""层级遍历""连通性判断" 类问题,前端常见场景包括:
- DOM 树 / 虚拟 DOM 的层级遍历(如组件渲染顺序控制);
- 前端路由的权限校验(从当前路由开始,逐层检查父路由权限);
- 小程序 / 游戏中的地图寻路(最短路径计算);
- 树形组件的展开 / 折叠逻辑(层级数据处理);
- 网络请求的并发控制(类似队列的任务调度)。
二、JS 实现 BFS 算法

(一)准备工作:创建图的数据结构
在实现 BFS 前,我们需要先定义一个 "图"(节点和边的集合)。在 JS 中,最直观的方式是用 对象(Object) 表示图:键为节点,值为该节点的邻居数组。
例如,一个简单的无向图可以这样定义:
js
// 图的结构:{ 节点: [邻居1, 邻居2, ...] }
const graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E']
};
这个图的结构可以理解为:A 和 B、C 相连,B 和 A、D、E 相连,以此类推。
(二)BFS 算法的具体实现步骤
基于上面的图,我们来实现 BFS:
- 初始化队列和访问记录 :用数组模拟队列(
shift
方法出队,push
方法入队),用 Set 存储已访问节点; - 处理队列:每次取出队头节点,打印(或执行其他操作);
- 入队邻居:遍历当前节点的所有邻居,若未访问则入队并标记为已访问。
(三)完整的 JS 代码实现
js
/**
* BFS算法实现
* @param {Object} graph 图的结构(节点: 邻居数组)
* @param {string} start 起始节点
* @returns {Array} 遍历结果(节点访问顺序)
*/
function bfs(graph, start) {
// 1. 初始化队列(放入起始节点)和访问记录
const queue = [start];
const visited = new Set();
visited.add(start); // 标记起始节点为已访问
// 存储遍历结果
const result = [];
// 2. 当队列不为空时,循环处理
while (queue.length > 0) {
// 3. 取出队头节点(队列特性:先进先出)
const current = queue.shift();
// 记录访问顺序
result.push(current);
// 4. 遍历当前节点的所有邻居
for (const neighbor of graph[current]) {
// 若邻居未访问,则入队并标记
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return result;
}
// 测试:从节点A开始遍历
const graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E']
};
console.log(bfs(graph, 'A')); // 输出:['A', 'B', 'C', 'D', 'E', 'F']
代码关键点解析:
- 用
queue.shift()
取出队头(JS 数组的shift
是 O (n) 操作,实际开发中可优化为用链表实现队列); visited
集合用于避免循环访问(比如图中 A 和 B 相互连接,若不标记会导致 A→B→A→B 的死循环);- 遍历结果
['A', 'B', 'C', 'D', 'E', 'F']
符合 "逐层访问" 的特性:先访问 A(第 0 层),再访问 B、C(第 1 层),最后访问 D、E、F(第 2 层)。
三、BFS 算法实战:迷宫最短路径问题

理论讲完了,我们用一个经典问题实战 ------迷宫最短路径(前端小游戏中很常见的需求)。
(一)问题描述
假设有一个 m×n 的迷宫,用二维数组表示:
-
0
表示可通行的空地; -
1
表示障碍物; -
起点为
(0,0)
,终点为(m-1, n-1)
; -
只能上下左右四个方向移动。
要求:找到从起点到终点的最短步数(若无法到达,返回 - 1)。
(二)解题思路
BFS 天生适合解决 "最短路径" 问题 ------ 因为它的逐层扩散特性,第一次到达终点时的步数就是最短路径(比 DFS 更高效,不需要回溯)。
实现要点:
- 用队列存储 "当前位置 + 当前步数"(如
[x, y, steps]
); - 用二维数组
visited
标记已访问的位置(避免重复走回头路); - 到达终点时直接返回当前步数。
(三)代码实现与分析
js
/**
* 迷宫最短路径(BFS解法)
* @param {number[][]} maze 迷宫二维数组(0可走,1障碍物)
* @returns {number} 最短步数(-1表示无法到达)
*/
function mazeShortestPath(maze) {
// 边界判断:迷宫为空或起点/终点是障碍物
if (!maze.length || maze[0][0] === 1 || maze[maze.length-1][maze[0].length-1] === 1) {
return -1;
}
const m = maze.length; // 行数
const n = maze[0].length; // 列数
const visited = Array.from({ length: m }, () => new Array(n).fill(false)); // 访问标记
const queue = [[0, 0, 0]]; // 队列:[x, y, steps],起点(0,0)步数为0
visited[0][0] = true;
// 上下左右四个方向(dx, dy)
const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
while (queue.length > 0) {
const [x, y, steps] = queue.shift(); // 取出当前位置和步数
// 若到达终点,直接返回步数(BFS保证这是最短路径)
if (x === m - 1 && y === n - 1) {
return steps;
}
// 遍历四个方向
for (const [dx, dy] of directions) {
const newX = x + dx;
const newY = y + dy;
// 判断新位置是否合法:在迷宫内 + 可通行 + 未访问
if (
newX >= 0 && newX < m && // x在有效范围
newY >= 0 && newY < n && // y在有效范围
maze[newX][newY] === 0 && // 不是障碍物
!visited[newX][newY] // 未访问过
) {
visited[newX][newY] = true; // 标记访问
queue.push([newX, newY, steps + 1]); // 入队,步数+1
}
}
}
// 队列为空仍未找到终点,返回-1
return -1;
}
// 测试用例
const maze = [
[0, 1, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 1, 0],
[1, 1, 1, 1, 0],
[0, 0, 0, 0, 0]
];
console.log(mazeShortestPath(maze)); // 输出:8(最短路径需要8步)
代码优势:
- 时间复杂度 O (m×n)(最多遍历所有格子一次);
- 空间复杂度 O (m×n)(visited 数组和队列的最大存储量);
- 一旦找到终点就返回,不需要遍历所有可能路径,效率很高。
四、BFS 算法的优化与拓展

(一)BFS 的性能优化
-
队列优化 :JS 中
shift()
方法是 O (n) 操作(需要重新排列数组),可改用deque
思想(用指针标记队头,避免数组整体移动):js// 用指针优化队列(避免shift的性能问题) let queue = [start]; let front = 0; // 队头指针 while (front < queue.length) { const current = queue[front++]; // 取出队头并移动指针 // ...处理current }
-
双向 BFS :当起点和终点明确时,可从起点和终点同时开始 BFS,相遇时的步数之和就是最短路径(适用于大型图,能减少一半搜索量)。
(二)BFS 的拓展应用
- 多源 BFS:从多个起点同时开始扩散(如 "感染问题":多个感染源同时扩散,求全部感染的时间);
- 层级遍历:二叉树的层序遍历(力扣 102 题)、N 叉树的层级遍历(力扣 429 题);
- 拓扑排序:解决依赖关系问题(如前端模块加载顺序)。
五、总结与展望

BFS 作为一种基础搜索算法,核心在于 "队列 + 逐层扩散" 的思想。它不像动态规划那样需要复杂的状态转移,也不像贪心算法那样依赖严格的条件,只要问题符合 "层级访问" 或 "最短路径" 特性,BFS 往往是最简单直接的解法。
作为前端工程师,掌握 BFS 不仅能应对算法题,更能在实际开发中找到更优的业务实现方案(比如优化组件渲染顺序、处理树形数据等)。
最后给大家留一个小练习:用 BFS 实现 "前端组件依赖加载"------ 已知组件 A 依赖 B 和 C,B 依赖 D,C 依赖 D,求正确的加载顺序(答案:D→B→C→A)。
算法学习就像拼图,每掌握一个知识点,就离 "解决复杂问题" 更近一步。下次遇到类似问题时,不妨问问自己:"用 BFS 能解决吗?"
如果觉得这篇文章有用,欢迎点赞收藏,也可以在评论区聊聊你在项目中用到 BFS 的场景~