
壹 ❀ 引
坦白的說,本人的算法簡直一塌糊塗,雖然有刷過一段時間的算法題,但依然只能解決不算復雜的問題,稍微麻煩的問題都只是站在能不能解決問題的角度,至於性能優化,算法方法的選擇並沒有過於深刻的理解。較巧的是,最近在工作中正好遇到了一個實際場景,整體修改下來也算感受頗深,便記錄於此,做個小分享,那么本文開始。
貳 ❀ 乞丐版深度優先
需求其實很簡單,比如企業微信中有人員組織架構,類似如下:

簡單來理解就是,一個公司的員工會被划分到多個部門,比如一級部門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環節未通過(我算法爛,其實心里也感覺通過不了),得到的反饋如下:
- 效率太低,一次只能查詢一個部門id,應該支持批量查詢
- 遍歷次數過多,最后為什么不用
join
直接拼接name
。 - 不應該是深度優先,改為廣度優先
叄 ❀ 廣度優先優化
我看到反饋其實心里也有疑慮,就找到了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,這時候廣度優先會更明智,之所以你寫了那么多非必要代碼,就是因為在廣度優先的場景下你用了深度優先。
我雖然之前了解廣度深度的概念,但確實缺少實際場景的經驗,就像你學了知識,該用到的時候你完全就沒有這個概念。所以這一次也算是自己對於廣度深度的一次不錯的理解了,那么本文結束。