「图/树的遍历与搜索」算法系统【前端例子版】

目录

什么是「搜索/遍历」?

任何 一个节点连着若干其他节点 的结构都叫做

是没有环的图。

  • 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(字典树) 前缀树 搜索框自动补全、敏感词过滤