在面試或者技術社區衝浪的時候,一不當心就會看到深度優先搜索、廣度優先搜索這兩個概念,這一次在項目中一個需求用到了相關的知識,故此在這裏經過理論+實際來總結一下。node
下面一張圖能夠比較理解二者的差別:面試
爲了後面好操做,咱們先定義一組平行節點爲如下,假想有一個共同的根節點:算法
const root = [
{
id: '1',
children: [
{
id: '1-1',
children: [{ id: '1-1-1' }, { id: '1-1-2' }],
},
{
id: '1-2',
children: [{ id: '1-2-1' }, { id: '1-2-2' }],
},
],
},
{
id: '2',
children: [
{
id: '2-1',
children: [{ id: '2-1-1' }, { id: '2-1-2' }],
},
{
id: '2-2',
children: [{ id: '2-2-1' }, { id: '2-2-2' }],
},
],
},
{
id: '3',
children: [
{
id: '3-1',
children: [{ id: '3-1-1' }, { id: '3-1-2' }],
},
{
id: '3-2',
children: [{ id: '3-2-1' }, { id: '3-2-2' }],
},
],
},
];
const target = '2-2-2';
複製代碼
深度優先搜索(depth first search),從圖中也能夠看出來,是從根節點開始,沿樹的深度進行搜索,儘量深的搜索分支。當節點所在的邊都已經搜多過,則回溯到上一個節點,再搜索其他的邊。數組
深度優先搜索採用棧結構,後進先出。iview
算法:ui
js 遞歸實現和非遞歸實現:spa
const depthFirstSearchWithRecursive = source => {
const result = []; // 存放結果的數組
// 遞歸方法
const dfs = data => {
// 遍歷數組
data.forEach(element => {
// 將當前節點 id 存放進結果
result.push(element.id);
// 若是當前節點有子節點,則遞歸調用
if (element.children && element.children.length > 0) {
dfs(element.children);
}
});
};
// 開始搜索
dfs(source);
return result;
};
const depthFirstSearchWithoutRecursive = source => {
const result = []; // 存放結果的數組
// 當前棧內爲所有數組
const stack = JSON.parse(JSON.stringify(source));
// 循環條件,棧不爲空
while (stack.length !== 0) {
// 最上層節點出棧
const node = stack.shift();
// 存放節點
result.push(node.id);
// 若是該節點有子節點,將子節點存入棧中,繼續下一次循環
const len = node.children && node.children.length;
for (let i = len - 1; i >= 0; i -= 1) {
stack.unshift(node.children[i]);
}
}
return result;
};
複製代碼
廣度優先搜索(breadth first search),從圖中也能夠看出來,是從根節點開始,沿樹的寬度進行搜索,若是全部節點都被訪問,則算法停止。code
廣度優先搜索採用隊列的形式,先進先出。cdn
js 實現:blog
const breadthFirstSearch = source => {
const result = []; // 存放結果的數組
// 當前隊列爲所有數據
const queue = JSON.parse(JSON.stringify(source));
// 循環條件,隊列不爲空
while (queue.length > 0) {
// 第一個節點出隊列
const node = queue.shift();
// 存放結果數組
result.push(node.id);
// 當前節點有子節點則將子節點存入隊列,繼續下一次的循環
const len = node.children && node.children.length;
for (let i = 0; i < len; i += 1) {
queue.push(node.children[i]);
}
}
return result;
};
複製代碼
實際應用確定不止我遇到的這一個,舉例的話就以我本身的經歷爲例子了。
需求以下:
能夠建立組織層級,大層級下有小層級,能夠無限建立下去。同時,編輯的時候要將父層級所有列出來(iview 的 tree 以及 cascader 組件)。
簡單來講就是從樹中找到某個節點,並返回節點的路徑。
算法:
// 深度優先搜索
const findPathByDepthFirstSearch = source => {
const stack = JSON.parse(JSON.stringify(source));
const result = [];
const dfs = data => {
// 保存當前節點
// (在路口灑下面包屑)
result.push(data);
// 當前節點的值爲真,則返回路徑
//(若是這個路口的終點是生門,經過記錄的麪包屑就找到了路徑)
if (data.id === target) {
return result.map(r => r.id);
}
// 若是當前節點有子節點,則繼續查找子節點
//(若是這個路口後面還有分叉路口,就先去第一個分叉路口下的第一條路)
if (data.children && data.children.length > 0) {
return dfs(data.children[0]);
}
// 最後一個節點的值爲假,彈出路徑中的該節點
//(最後一個路口是死路,清理最後一個路口的麪包屑)
result.pop();
// 若是路徑數組爲空,則判斷源節點是否還有待搜索的節點
//(若是麪包屑都清空了,也就是回到了原點,那就看看還有沒有別的路口)
if (result.length === 0) {
return stack.length > 0 ? dfs(stack.shift()) : result;
}
// 獲取路徑中最後一個節點,是當前節點的父節點
//(去撒有面包屑的最後一個路口看看,當前路口的麪包屑已經在上面被清理了)
const lastNode = result[result.length - 1];
// 彈出路徑中最後節點中的第一個子節點(前面已經查找失敗了)
//(當前路子不夠野【在上面已經試過這條路,是死路】)
lastNode.children.shift();
// 查找最後一個有效節點的下一個子節點(前一個被 shift 了)
//(若是這個路口下還有其餘沒嘗試過的路,從第一條(實際是下一條了)開始嘗試)
if (lastNode.children.length > 0) {
return dfs(lastNode.children[0]);
}
// 最後節點下的子節點所有嘗試查找失敗,返回上一個節點查找
//(這個路口若是沒有其餘路了,清理麪包屑且去上一個路口的第二條路看看【本條是第一條路,已經走過了】)
return dfs(result.pop());
};
// 開始找路
return dfs(stack.shift());
};
複製代碼
算法:
代碼以下:
// 廣度優先搜索
const findPathByBreadthFirstSearch = source => {
let result = [];
let queue = JSON.parse(JSON.stringify(source));
while (queue.length > 0) {
// 遍歷隊列(隊列會動態增長)
//(從第一個路口開始試探)
for (let i = 0; i < queue.length; i += 1) {
// 獲取當前隊列的一項
// (這是一個路口)
const node = queue[i];
// 判斷節點是否爲目標節點
//(路口是否是生門?)
if (node.id === target) {
// 隊列清空
//(已經找到生門,不用再接着找了)
queue = [];
// 經過 parent 一層層查找路徑
//(從這個路口經過麪包屑【parent】找歸途,直到找到回家的路)
return (function findParent(data) {
result.unshift(data.id);
if (data.parent) {
return findParent(data.parent);
}
return result;
})(node);
}
// 節點有子節點,設置子節點的 parent 爲當前節點,推入隊列
//(這個路口下還有其餘路,先記住這個這個路口下的路是屬於如今這個路口的【parent】
// 而後去下一個路口,按順序來試)
if (node.children && node.children.length > 0) {
queue.push(
...node.children.map(leaf => {
leaf.parent = node;
return leaf;
})
);
}
}
}
return result;
};
複製代碼
通常來講,能用深度優先搜索的場景也能用廣度優先搜索,從大腦的思考方式來講,深度優先搜索更符合人們的認知行爲。與此同時,當節點足夠複雜,能夠考慮使用迭代深化深度優先搜索(重複運行一個有深度限制的深度優先搜索),時間複雜度與廣度優先搜索一致,而空間複雜度遠優。