• JS 记一次工作中,由深度优先到广度优先的算法优化


    壹 ❀ 引

    坦白的说,本人的算法简直一塌糊涂,虽然有刷过一段时间的算法题,但依然只能解决不算复杂的问题,稍微麻烦的问题都只是站在能不能解决问题的角度,至于性能优化,算法方法的选择并没有过于深刻的理解。较巧的是,最近在工作中正好遇到了一个实际场景,整体修改下来也算感受颇深,便记录于此,做个小分享,那么本文开始。

    贰 ❀ 乞丐版深度优先

    需求其实很简单,比如企业微信中有人员组织架构,类似如下:

    简单来理解就是,一个公司的员工会被划分到多个部门,比如一级部门A,B,C...,一级部门下也可能还有二级部门如A-1-1,同理二级部门A-1-1下可能还有三级部门。有些员工可能角色特殊,会同时属于多个不同层级的部门,现在需求就是,后端会返回一个该员工所属部门的ID数组,你需要在上述部门数据中利用ID数组查找出对应的部门名字(name),同时要按一级=>二级=>三级类似的顺序排列并组合成字符串作为展示。

    如下提供了模拟数据,因为2对应的是UI,层级比5的前端二组高,所以期望得到的结果为UI;前端二组,那么怎么做呢?

    // 某用户所在的部门id
    const departmentsId = [5,2];
    // 部门模拟数据
    const departmentTree = [
      {
        name: "技术部",
        id: 0,
        children: [
          {
            id: 1,
            name: "前端",
            children: [
              {
                id: 4,
                name: "前端一组",
                children: [],
              },
              {
                id: 5,
                name: "前端二组",
                children: [
                  {
                    id: 9,
                    name: "前端-移动端一组",
                    children: [],
                  },
                ],
              },
            ],
          },
          {
            id: 2,
            name: "UI",
            children: [
              {
                id: 6,
                name: "UI一组",
                children: [{ id: 10, name: "视觉设计", children: [] }],
              },
            ],
          },
          {
            id: 3,
            name: "后端",
            children: [
              {
                id: 7,
                name: "后端一组",
                children: [],
              },
              {
                id: 8,
                name: "后端二组",
                children: [],
              },
            ],
          },
        ],
      },
    ];
    

    我首先想到的就是遍历departmentIds,每次拿一个部门ID到departmentTree中查找,由于层级高的要排在前面,所以想到深度遍历,每往下一层都会记录当前的depth属性,如果找到了,最终会到如一个包含部门name和此部门depth的对象,因为是深度优先,所以需要结合递归,我的实现是这样:

    const getDeptNameFromTreeDepts = (treeDepartments, departmentid) => {
      let departmentNameInfo = "";
      function departmentTraversal(node, depth) {
        // 这里利用find,找到了就没必要继续后续查找了
        const targetDepartment = node.find((department) => {
          // 判断当前部门id是否和提供的id相同
          return department.id === departmentid
            ? true
            : department.children && //如果不相同,判断有没有children,同时让depth加1
                department.children.length &&
                departmentTraversal(department.children, depth + 1);
        });
        // 记录当前部门name和depth属性
        if (targetDepartment) {
          departmentNameInfo = {
            name: targetDepartment.name,
            depth: depth + 1,
          };
        }
      }
      treeDepartments.forEach(({ children }) => {
        departmentTraversal(children, 0);
      });
      return departmentNameInfo;
    };
    //用于记录每次查找到的结果
    const departmentNames = [];
    // 遍历人员所属部门id,一次查找一个。
    departmentIds.forEach((departmentId) => {
      const transformedDepartment = getDeptNameFromTreeDepts(
        departmentTree,
        departmentId
      );
      if (transformedDepartment) {
        departmentNames.push(transformedDepartment);
      }
    });
    // 根据depth属性来决定部门先后并做name拼接
    const name = departmentNames
      .sort((a, b) => {
        return a.depth - b.depth;
      })
      .reduce((acc, cur) => {
        return `${acc + cur.name}; `;
      }, "");
    console.log(name);// UI; 前端二组; 
    

    代码看着有点多,确实比较复杂,抛开递归不说,最终得到了目标数组,还需要根据depth属性对部门name排序,之后再做字符拼接,而且得到的结果是UI; 前端二组; ,我们想要的是UI; 前端二组,尾部还多了一个;,理论上来说还要做一次额外处理。在发版前提测测试通过没问题,很遗憾,在code review环节未通过(我算法烂,其实心里也感觉通过不了),得到的反馈如下:

    1. 效率太低,一次只能查询一个部门id,应该支持批量查询
    2. 遍历次数过多,最后为什么不用join直接拼接name
    3. 不应该是深度优先,改为广度优先

    叁 ❀ 广度优先优化

    我看到反馈其实心里也有疑虑,就找到了review的前辈,说之所以没用join是因为最终得到的数组并不是包含纯部门的name,而是多个对象数组,它们之前并无先后顺序,所以需要根据depth来做排序最后做拼接。

    前辈说不应该啊,你在查找的时候不是已经知道了多个目标部门的先后顺序了吗,为什么还要利用depth呢?我当时还没反应过来,问他难道在查找的时候顺便做一次插入排序?这样就能保证返回的结果已经带有顺序。他说广度优先不是从上到下一层一层的找吗?你直接把Tree遍历一次,每到了一个节点,看这个节点在不在departmentsId里面,如果在,那说明是你想要的,由于广度优先是一层层向下,所以你查找出来的结果已经自带层级排序了,写算法之前一定要先设计好自己的算法思路,这样才能少走弯路。我顿时恍然大悟!!!立马回去改了代码!!!

    一番修改,于是得到了广度优先的实现代码:

    const getDeptNameFromTreeDepts = (treeDepartments, departmentIds) => {
      const departmentNames = [];
      // 浅拷贝一份,不然在做队列操作时会影响原数据
      const queue = [...treeDepartments];
      while (queue.length > 0) {
        // 每次从对了头部取一个用于做对比
        const department = queue.shift();
        // 判断当前节点的部门uuid在不在departmentUuid数组中,在的话就是我们想要找的部门,并提取name属性
        if (departmentIds.includes(department.id)) {
          departmentNames.push(department.name);
        }
        // 将当前部门的部门的子部门加到队列尾部
        department.children && queue.push(...department.children);
      }
      return departmentNames.length > 0 ? departmentNames.join("; ") : "";
    };
    
    const name = getDeptNameFromTreeDepts(departmentTree, departmentIds);
    console.log(name);// UI; 前端二组
    

    所以实现到这,我整个人都傻了,代码量直接少了一大半不说,得到的结果也不需要做额外处理。其次,我们在前面的实现中,虽然可以保证不同层级的部门排序,但却无法保证同级部门的先后排序。比如第一层级部门顺序为前端、UI、后端,在前面的实现中,我们是依次拿departmentsId中的id去查找,所以同级的先后顺序其实被departmentsId中id的先后顺序给决定了,在前的会被先找到,例如前面的实现,假设departmentsId[3,2],查询出来的结果就是后端;UI,与Tree的顺序并不一致。

    而在后续的修改中,同级顺序的问题也不需要考虑了,因为我们一共就遍历了一次Tree,同级节点如果满足,自然会被先找到,所以这段修改不仅解决了不同层级的先后排序,同时也满足了同级部门顺序与Tree一致,大功告成。

    肆 ❀ 总

    虽然这并不是一次很难的需求,但是在改完代码之后,老实说我心里感触很大。本人算法确实一塌糊涂,很多场景只是能达到实现的角度,缺少了对于算法选择的大局观。正如前辈所说,有时候的你的算法选择应该更去贴合需求,如果你期望结果是ABAB,很明显你应该用深度优先,但如果你要的是AABB,这时候广度优先会更明智,之所以你写了那么多非必要代码,就是因为在广度优先的场景下你用了深度优先。

    我虽然之前了解广度深度的概念,但确实缺少实际场景的经验,就像你学了知识,该用到的时候你完全就没有这个概念。所以这一次也算是自己对于广度深度的一次不错的理解了,那么本文结束。

  • 相关阅读:
    用代理IP进行简单的爬虫——爬高匿代理网站
    python利用django实现简单的登录和注册,并利用session实现了链接数据库
    python基础知识——基于python3.6
    笔记2_列表、元组、字典
    wpf Command
    可枚举对象操作
    2019寒假训练营寒假作业(二) 程序题部分
    2019-01-23 寒假作业(一)
    2019寒假训练营第二次作业
    网络空间安全概论 学习笔记(二)
  • 原文地址:https://www.cnblogs.com/echolun/p/13933431.html
Copyright © 2020-2023  润新知