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,這時候廣度優先會更明智,之所以你寫了那么多非必要代碼,就是因為在廣度優先的場景下你用了深度優先。

我雖然之前了解廣度深度的概念,但確實缺少實際場景的經驗,就像你學了知識,該用到的時候你完全就沒有這個概念。所以這一次也算是自己對於廣度深度的一次不錯的理解了,那么本文結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM