树了个树,回溯源头

864 阅读3分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

有人相爱,有人夜里开车看海,有人leetcode第一题都做不出来

背景

在实现中台服务时,我们要按系统模块依赖关系去从当前点击模块,向上回溯找到与当前节点所有直接关联的模块,同时需还原每个梯队所包含的模块,最后将相关梯队数据给到运维,由其从上到下有序调度编译任务。在实现回溯这颗树时,我们需要一点点关于树相关遍历的基础。一起先从回顾树结构及其算法开始吧。

算法回顾

树结构:

  1. 概念:一种分层数据的抽象模型。
  2. 应用:在前端开发程序中围绕树结构及其算法是使用频率最高的一种。例如:dom tree, Vnode,级联选择(Cascader), Tree组件,多层菜单等,业务中常见的流程图、拓扑图、任务编排、节点关系等都有其应用场景。
  3. 两种常见形式:
 const data = {
    id: '1',
    children: [
      { 
        id: 2,
        pid: 1,
        children: [{id: 3, pid: 2}],
      }
    ]
  }
const nodes = [
    {id: 1, ...},
    {id: 2, ...},
]
const edges = [{
    source: '1',
    target: '2',
    ...
}]

4.树结构常用算法:深度/广度优先遍历、(二叉树)先中后序遍历。

5.深度优先遍历(dfs):

/*
* 尽可能深的搜索树的分支
*/
cont dfs = (root) => {
 console.log(root.val); // 1、先访问根节点
 root?.children.forEach(dfs); // 2、对根节点的children挨个进行深度优先遍历
}

image.png

6.广度优先遍历(bfs):

/*
* 尽可能访问离根节点最近的节点
*/
const bfs = (root) => {
 const queue = [root];  // 1、构建一个队列,把根节点入队
 while(queue.length > 0){ 
   const head = queue.shift();   // 2、把队头出队并访问
   console.log(head); // 访问当前节点
   (head?.children || []).forEach(child => queue.push(child)) // 3、把队头的children 挨个入队
 }
  // 4、重复 1-3步
}

image.png

7.Playground:

实现

1、数据可视化: 关于模块依赖关系通过数据可视化比较简单。首先构建可视化数据(包含两部分,其一是nodes描述模块信息,其二edges来维护实体模块关系。)然后将上述构建好的数据丢给AntV即可。效果如图1-1

image.png

图1-1 示意图

2、回溯源头: 有了数据可视化,我们可以清晰的看到每一梯队有哪些模块,模块和模块之间的依赖关系等。此时我们要点击其中任一模块通知运维执行编译操作,但是运维不支持单模块编译,只能给到当前模块及其所有依赖模块后整体按需编译到当前模块。我们就要实现从起点到源头的一个回溯过程。抽象模型见图1-2

image.png

图1-2 回溯过程

由于全量源数据是一个打平的树结构(nodes、edges),我们在递归遍历操作时无法依赖children字段来判断是否进行下去,这里给出的方案是通过结合广度遍历和深度遍历思想(见算法回顾)相结合的方式,首先将当前节点的父元素集合构建一个队列,然后执行递归操作,最后通过节点层级维护同一梯队的ids。 最终实现打印结果如下

// id集合
const pidMap = new Map();
// 向上回溯
const backtracking = (targetId, level) => {
    if (pidMap.has(level)) {
      pidMap.get(level).add(targetId + '')
    } else {
      pidMap.set(level, new Set().add(targetId + ''))
    }
    const { edges = [] } = graphData;
    const pidNodes = edges.filter(item => item.target === targetId)
    while (Array.isArray(pidNodes) && pidNodes.length) {
      let pidNode = pidNodes.pop();
      if (pidNode && pidNode.source) {
        backtracking(pidNode.source, level+1)
      }
    }
}
const rankData = [...pidMap.values()].map(item => Array.from(item))
console.log(rankData) 
// [Array(1), Array(1), Array(3), Array(1)]
// 0: ['10'] 当前节点
// 1: ['7'] 第三梯队
// 2: (3) ['4', '3', '2'] 第二梯队
// 3: ['0'] 源头

完整试验代码

code.juejin.cn/pen/7148412…

总结

最近🐑了个🐑比较火,蹭个热度,搞一个树了个树,学以致用,活学活用,在刷题的路上共勉。