現在有一種類似樹的數據結構,但是不存在共同的根節點 root,每一個節點的結構為 {key: 'one', value: '1', children: [...]}
,都包含 key
和 value
,如果存在 children
則內部會存在 n
個和此結構相同的節點,現模擬數據如下圖:
已知一個 value
如 3-2-1
,需要取出該路徑上的所有 key
,即期望得到 ['three', 'three-two', 'three-two-one']
。
1.廣度優先遍歷
廣度優先的算法如下圖:
從上圖可以輕易看出廣度優先即是按照數據結構的層次一層層遍歷搜索。
首先需要把外層的數據結構放入一個待搜索的隊列(Queue)中,進而對這個隊列進行遍歷,當正在遍歷的節點存在子節點(children)時則把此子節點下所有節點放入待搜索隊列的末端。
因為本需求需要記錄路徑,因此還需要對這些數據做一些特殊處理,此處采用了為這些節點增加 parent 即來源的方法。
對此隊列依次搜索直至找到目標節點時,可通過深度遍歷此節點的 parent 從而獲得到整個目標路徑。具體代碼如下:
-
// 廣度優先遍歷
-
function findPathBFS(source, goal) {
-
// 深拷貝原始數據
-
var dataSource = JSON.parse(JSON.stringify(source))
-
var res = []
-
// 每一層的數據都 push 進 res
-
res.push(...dataSource)
-
// res 動態增加長度
-
for (var i = 0; i < res.length; i++) {
-
var curData = res[i]
-
// 匹配成功
-
if (curData.value === goal) {
-
var result = []
-
// 返回當前對象及其父節點所組成的結果
-
return (function findParent(data) {
-
result.unshift(data.key)
-
if (data.parent) return findParent(data.parent)
-
return result
-
})(curData)
-
}
-
// 如果有 children 則 push 進 res 中待搜索
-
if (curData.children) {
-
res.push(...curData.children.map( d => {
-
// 在每一個數據中增加 parent,為了記錄路徑使用
-
d.parent = curData
-
return d
-
}))
-
}
-
}
-
// 沒有搜索到結果,默認返回空數組
-
return []
-
}
2.深度優先遍歷
深度優先的算法如下圖:
深度優先即是取得要遍歷的節點時如果發現有子節點(children
) 時,則不斷的深度遍歷,並把這些節點放入一個待搜索的棧(Stack)中,直到最后一個沒有子節點的節點時,開始對棧進行搜索。后進先出(下列代碼中使用了 push
方法入棧,因此需使用 pop
方法出棧),如果沒有匹配到,則刪掉此節點,同時刪掉父節點中的自身,不斷重復遍歷直到匹配為止。注意,常規的深度優先並不會破壞原始數據結構,而是采用 isVisited
或者顏色標記法進行表示,原理相同,此處簡單粗暴做了刪除處理。代碼如下:
-
// 深度優先遍歷
-
function findPathDFS(source, goal) {
-
// 把所有資源放到一個樹的節點下,因為會改變原數據,因此做深拷貝處理
-
var dataSource = [{children: JSON.parse(JSON.stringify(source))}]
-
var res = []
-
return (function dfs(data) {
-
if (!data.length) return res
-
res.push(data[ 0])
-
// 深度搜索一條數據,存取在數組 res 中
-
if (data[0].children) return dfs(data[0].children)
-
// 匹配成功
-
if (res[res.length - 1].value === goal) {
-
// 刪除自己添加樹的根節點
-
res.shift()
-
return res.map(r => r.key)
-
}
-
// 匹配失敗則刪掉當前比對的節點
-
res.pop()
-
// 沒有匹配到任何值則 return
-
if (!res.length) return res
-
// 取得最后一個節點,待做再次匹配
-
var lastNode = res[res.length - 1]
-
// 刪除已經匹配失敗的節點(即為上面 res.pop() 的內容)
-
lastNode.children.shift()
-
// 沒有 children 時
-
if (!lastNode.children.length) {
-
// 刪除空 children,且此時需要深度搜索的為 res 的最后一個值
-
delete lastNode.children
-
return dfs([res.pop()])
-
}
-
return dfs(lastNode.children)
-
})(dataSource)
-
}
該方法在思考時,添加了根節點以把數據轉換成樹,並在做深度遍歷時傳入了子節點數組 children
作為參數,其實多有不便,於是優化后的代碼如下:
-
// 優化后的深度搜索
-
function findPathDFS(source, goal) {
-
// 因為會改變原數據,因此做深拷貝處理
-
var dataSource = JSON.parse(JSON.stringify(source))
-
var res = []
-
return (function dfs(data) {
-
res.push(data)
-
// 深度搜索一條數據,存取在數組 res 中
-
if (data.children) return dfs(data.children[0])
-
// 匹配成功
-
if (res[res.length - 1].value === goal) {
-
return res.map(r => r.key)
-
}
-
// 匹配失敗則刪掉當前比對的節點
-
res.pop()
-
// 沒有匹配到任何值則 return,如果源數據有值則再次深度搜索
-
if (!res.length) return !!dataSource.length ? dfs(dataSource.shift()) : res
-
// 取得最后一個節點,待做再次匹配
-
var lastNode = res[res.length - 1]
-
// 刪除已經匹配失敗的節點(即為上面 res.pop() 的內容)
-
lastNode.children.shift()
-
// 沒有 children 時
-
if (!lastNode.children.length) {
-
// 刪除空 children,且此時需要深度搜索的為 res 的最后一個值
-
delete lastNode.children
-
return dfs(res.pop())
-
}
-
return dfs(lastNode.children[0])
-
})(dataSource.shift())
-
}
改進后的方法只關心傳入的節點,如果存在子節點則內部自行處理,而非預先傳入所有子節點數組進行處理,此方法更易理解一些。
結語
以上便是廣度與深度遍歷在 JS 中的應用,代碼可在 codepen 中查看。