目录
- 什么是「搜索/遍历」?
- [BFS - 广度优先搜索(Breadth-First Search)](#BFS - 广度优先搜索(Breadth-First Search))
- [DFS - 深度优先搜索(Depth-First Search)](#DFS - 深度优先搜索(Depth-First Search))
- DFS的三种细分顺序(针对二叉树/有序数)
- 层序遍历(Level-Order)其实就是BFS
- [带权图的搜索 - Dijkstra、A*](#带权图的搜索 - Dijkstra、A*)
-
- Dijkstra算法
- 前端例子:
- [A* 算法](#A* 算法)
- 综合前端示例:在DOM树里找元素
- 怎么选?
- 兄弟概念速览
什么是「搜索/遍历」?
任何 一个节点连着若干其他节点 的结构都叫做 图 。
树 是没有环的图。
- DOM树: document 是根,每个元素的children是子节点
- 虚拟DOM(React/Vue的VDOM):组件树
- 文件目录树:node_modules里的依赖
- 路由表:React Router的嵌套路由
- Promise链/时间冒泡路径
遍历就是按照某种顺序把所有节点访问一遍;搜索就是在遍历过程中找到目标就停。
BFS - 广度优先搜索(Breadth-First Search)
核心
像水波一样一层一层往外扩散。用队列(Queue,FIFO)
javascript
A ← 第 0 层
/ \
B C ← 第 1 层
/ \ \
D E F ← 第 2 层
BFS 顺序:A → B → C → D → E → F
例子
前端例子:查找 DOM 树中第一个 .btn 元素(按层级最近优先)
javascript
function bfsFindClass(root, className) {
const queue = [root];
while (queue.length) {
const node = queue.shift(); // 从队首拿
if (node.classList?.contains(className)) return node;
queue.push(...node.children); // 子节点入队尾
}
return null;
}
典型应用
- 最短路径(无权图):地铁换乘(每站权重一样:A站 -> B站 -> C站 -> D站)、朋友推荐里的「二度人脉」
- DOM层序查找: 找离根最近的某元素
- 图片懒加载的可见性扫描:从视口中心往外扩
- React Fiber早起的「层序提交」思路
- 网页爬虫:先抓首页,再抓首页链接的页面
DFS - 深度优先搜索(Depth-First Search)
核心
一条路走到黑,走不通再回头。用栈(Stack,LIFO)或递归(递归本质就是函数调用栈)
javascript
A
/ \
B C
/ \ \
D E F
DFS 顺序(先左后右):A → B → D → E → C → F
例子
前端例子:递归渲染嵌套菜单
javascript
function dfsRender(node, depth = 0) {
console.log(' '.repeat(depth * 2) + node.label); // 每深入一层,多缩进2个空格
node.children?.forEach(child => dfsRender(child, depth + 1));
}
迭代版(用显示栈)
javascript
function dfsIter(root) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
console.log(node.value);
if (node.children) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
}
典型应用
- JSON深拷贝/序列化
- VDOM diff: React的协调过程本质就是DFS
- 路径查找:从一个组件找到它的根
- 检测循环依赖:webpack/vite解析模块图
- JSON.stringify: 内部就是DFS
DFS的三种细分顺序(针对二叉树/有序数)
这个很重要,前端做AST(抽象语法树)处理、Babel插件、EsLint规则全靠它。(前端工程化工具其实都在遍历一棵树,而遍历树最核心的就是 DFS)
javascript
1
/ \
2 3
/ \
4 5
| 顺序 | 访问时机 | 结果 | 典型用途 |
|---|---|---|---|
| 前序 Preorder | 根->左->右 | 1,2,4,5,3 | 复制树、生成目录、Babel插件 |
| 中序 Inorder | 左->根->右 | 4,2,5,1,3 | 二叉搜索树顺序输出 |
| 后序 Postorder | 左->右->根 | 4,5,2,3,1 | 释放节点、计算依赖(先算孩子再算自己) |
例子
Babel访问AST
javascript
// Babel 插件就是一个访问者,enter 是前序,exit 是后序
traverse(ast, {
CallExpression: {
enter(path) { /* 前序:进入节点时 */ },
exit(path) { /* 后序:孩子都处理完时 */ }
}
});
React 的 componentDidMount 是后序触发的(子组件先挂载完,父组件才挂载完),这就是为什么父组件的 didMount 里能拿到子组件的 DOM。
层序遍历(Level-Order)其实就是BFS
经常被单独拿出来说,因为很多面试题就叫做 "层序遍历"。
javascript
A
/ \
B C
/ \ / \
D E F G
按层访问:
javascript
第1层:A
第2层:B C
第3层:D E F G
结果就是:
javascript
A B C D E F G
对比DFS:
前序:A B D E C F G
后序:D E B F G C A
可以看到:DFS一直往下钻;而BFS先把同层走完
前端场景:
- 渲染「每层不同样式的」的菜单
- 做面包屑导航
- DOM层级查找
- 评论树
例子:
返回 \[A, B,C, D,E,F] 这种按层分组的结构
针对多叉树:
javascript
function levelOrder(root) {
const result = [], queue = [root];
while (queue.length) {
const level = [], size = queue.length;
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.val);
if (node.children) {
queue.push(...node.children);
}
}
result.push(level);
}
return result;
}
假设树长这样:
javascript
A
/ | \
B C D
/ \
E F
对应的数据就是:
javascript
const root = {
val: 'A',
children: [
{
val: 'B',
children: [
{ val: 'E' },
{ val: 'F' }
]
},
{ val: 'C' },
{ val: 'D' }
]
};
针对二叉树:
javascript
function levelOrder(root) {
const result = [];
const queue = [root];
while (queue.length) {
const size = queue.length;
const level = [];
for (let i = 0; i < size; i++) {
const node = queue.shift();
level.push(node.value);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
result.push(level);
}
return result;
}
javascript
A
/ \
B C
/ \
D E
带权图的搜索 - Dijkstra、A*
BFS适合「每条边代价相同」的图。如果边有权重(路费、距离)就要升级:
Dijkstra算法
核心: BFS的升级版,把队列换成优先队列(小顶堆),每次取「当前累计代价最小的」的节点。(每次都选择当前距离最小的点继续扩散)
前端例子:
- 地图导航 SDK(高德/百度的网页版)
- 网络请求重试策略:选延迟最小的镜像源
- React调度器: Scheduler内部用最小堆按优先级取任务
javascript
// 简化版伪代码
function dijkstra(graph, start) {
const dist = { [start]: 0 };
const pq = new MinHeap([[0, start]]);
while (!pq.empty()) {
const [d, node] = pq.pop();
for (const [next, weight] of graph[node]) {
const nd = d + weight;
if (nd < (dist[next] ?? Infinity)) {
dist[next] = nd;
pq.push([nd, next]);
}
}
}
return dist;
}
A* 算法
核心 :Dijkstra + 启发函数(heuristic)。除了"已走的代价",还估计"到终点的剩余代价",更聪明。
前端例子: 游戏寻路(用 Phaser、Cocos 做的网页游戏,怪物追玩家走的路径就是 A*)
综合前端示例:在DOM树里找元素
javascript
// 假设页面里有多个 .target,分别比较 BFS 和 DFS 找到的第一个
// BFS:找到的是「层级最浅」的那个(离 root 最近)
function findBFS(root, selector) {
const q = [root];
while (q.length) {
const n = q.shift();
if (n.matches?.(selector)) return n;
q.push(...n.children);
}
}
// DFS:找到的是「文档顺序最靠前」的那个(querySelector 的行为)
function findDFS(root, selector) {
if (root.matches?.(selector)) return root;
for (const c of root.children) {
const found = findDFS(c, selector);
if (found) return found;
}
}
document.querySelector 用的就是 DFS(具体说是前序遍历),所以它返回的是源码中第一个匹配的元素,不一定是层级最浅的。
怎么选?
| 需求 | 用谁 |
|---|---|
| 找最短路径(步数最少) | BFS |
| 找最短路径(带权重) | Dijkstra / A* |
| 遍历所有节点、做不在乎顺序的事 | DFS(写起来简单,递归就行) |
| 处理"先孩子后自己"的逻辑(释放、求值) | DFS后序 |
| 处理"先自己后孩子"(复制、克隆) | DFS 前序 |
| 树很深,递归会爆栈 | ** BFS 或 DFS 迭代版** |
| 有依赖关系,要排顺序 | 拓扑排序 |
兄弟概念速览
| 名字 | 一句话 | 前端关联 |
|---|---|---|
| IDDFS(迭代加深DFS) | DFS但限制深度,逐层加深 | 兼顾BFS找最浅解 + DFS省内存 |
| 双向BFS | 起点终点同时BFS,相遇即停 | 六度人脉查询、社交网络 |
| 拓扑排序(Topo Sort) | 有向无环图的"前后顺序" | npm依赖安装顺序、webpack chunk加载顺序、vue的computed求值顺序 |
| Tarjan / Kosaraju | 找强连通分量 | 循环依赖检测(webpack报circular dependency 就是它) |
| Floyd-Warshall | 所有点对之间最短路 | 全连通缓存表 |
| Union-Find | 快速判断"两个节点是否连通" | 拖拽组合、画板里的连线合并 |
| Trie(字典树) | 前缀树 | 搜索框自动补全、敏感词过滤 |