JS 中的廣度與深度優先遍歷


現在有一種類似樹的數據結構,但是不存在共同的根節點 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 從而獲得到整個目標路徑。具體代碼如下:

  1.  
    // 廣度優先遍歷
  2.  
    function findPathBFS(source, goal) {
  3.  
      // 深拷貝原始數據
  4.  
       var dataSource = JSON.parse(JSON.stringify(source))
  5.  
           var res = []
  6.  
          // 每一層的數據都 push 進 res
  7.  
         res.push(...dataSource)
  8.  
         // res 動態增加長度
  9.  
        for (var i = 0; i < res.length; i++) {
  10.  
        var curData = res[i]
  11.  
           // 匹配成功
  12.  
          if (curData.value === goal) {
  13.  
              var result = []
  14.  
             // 返回當前對象及其父節點所組成的結果
  15.  
            return (function findParent(data) {
  16.  
                 result.unshift(data.key)
  17.  
               if (data.parent) return findParent(data.parent)
  18.  
                  return result
  19.  
           })(curData)
  20.  
       }
  21.  
      // 如果有 children 則 push 進 res 中待搜索
  22.  
      if (curData.children) {
  23.  
          res.push(...curData.children.map( d => {
  24.  
           // 在每一個數據中增加 parent,為了記錄路徑使用
  25.  
           d.parent = curData
  26.  
           return d
  27.  
      }))
  28.  
     }
  29.  
     }
  30.  
      // 沒有搜索到結果,默認返回空數組
  31.  
     return []
  32.  
    }

 

2.深度優先遍歷

深度優先的算法如下圖:

 

深度優先即是取得要遍歷的節點時如果發現有子節點(children) 時,則不斷的深度遍歷,並把這些節點放入一個待搜索的棧(Stack)中,直到最后一個沒有子節點的節點時,開始對棧進行搜索。后進先出(下列代碼中使用了 push 方法入棧,因此需使用 pop 方法出棧),如果沒有匹配到,則刪掉此節點,同時刪掉父節點中的自身,不斷重復遍歷直到匹配為止。注意,常規的深度優先並不會破壞原始數據結構,而是采用 isVisited 或者顏色標記法進行表示,原理相同,此處簡單粗暴做了刪除處理。代碼如下:

  1.  
    // 深度優先遍歷
  2.  
    function findPathDFS(source, goal) {
  3.  
    // 把所有資源放到一個樹的節點下,因為會改變原數據,因此做深拷貝處理
  4.  
    var dataSource = [{children: JSON.parse(JSON.stringify(source))}]
  5.  
    var res = []
  6.  
    return (function dfs(data) {
  7.  
    if (!data.length) return res
  8.  
    res.push(data[ 0])
  9.  
    // 深度搜索一條數據,存取在數組 res 中
  10.  
    if (data[0].children) return dfs(data[0].children)
  11.  
    // 匹配成功
  12.  
    if (res[res.length - 1].value === goal) {
  13.  
    // 刪除自己添加樹的根節點
  14.  
    res.shift()
  15.  
    return res.map(r => r.key)
  16.  
    }
  17.  
    // 匹配失敗則刪掉當前比對的節點
  18.  
    res.pop()
  19.  
    // 沒有匹配到任何值則 return
  20.  
    if (!res.length) return res
  21.  
    // 取得最后一個節點,待做再次匹配
  22.  
    var lastNode = res[res.length - 1]
  23.  
    // 刪除已經匹配失敗的節點(即為上面 res.pop() 的內容)
  24.  
    lastNode.children.shift()
  25.  
    // 沒有 children 時
  26.  
    if (!lastNode.children.length) {
  27.  
    // 刪除空 children,且此時需要深度搜索的為 res 的最后一個值
  28.  
    delete lastNode.children
  29.  
    return dfs([res.pop()])
  30.  
    }
  31.  
    return dfs(lastNode.children)
  32.  
    })(dataSource)
  33.  
    }

該方法在思考時,添加了根節點以把數據轉換成樹,並在做深度遍歷時傳入了子節點數組 children 作為參數,其實多有不便,於是優化后的代碼如下:

  1.  
    // 優化后的深度搜索
  2.  
    function findPathDFS(source, goal) {
  3.  
    // 因為會改變原數據,因此做深拷貝處理
  4.  
    var dataSource = JSON.parse(JSON.stringify(source))
  5.  
    var res = []
  6.  
    return (function dfs(data) {
  7.  
    res.push(data)
  8.  
    // 深度搜索一條數據,存取在數組 res 中
  9.  
    if (data.children) return dfs(data.children[0])
  10.  
    // 匹配成功
  11.  
    if (res[res.length - 1].value === goal) {
  12.  
    return res.map(r => r.key)
  13.  
    }
  14.  
    // 匹配失敗則刪掉當前比對的節點
  15.  
    res.pop()
  16.  
    // 沒有匹配到任何值則 return,如果源數據有值則再次深度搜索
  17.  
    if (!res.length) return !!dataSource.length ? dfs(dataSource.shift()) : res
  18.  
    // 取得最后一個節點,待做再次匹配
  19.  
    var lastNode = res[res.length - 1]
  20.  
    // 刪除已經匹配失敗的節點(即為上面 res.pop() 的內容)
  21.  
    lastNode.children.shift()
  22.  
    // 沒有 children 時
  23.  
    if (!lastNode.children.length) {
  24.  
    // 刪除空 children,且此時需要深度搜索的為 res 的最后一個值
  25.  
    delete lastNode.children
  26.  
    return dfs(res.pop())
  27.  
    }
  28.  
    return dfs(lastNode.children[0])
  29.  
    })(dataSource.shift())
  30.  
    }

改進后的方法只關心傳入的節點,如果存在子節點則內部自行處理,而非預先傳入所有子節點數組進行處理,此方法更易理解一些。

結語

以上便是廣度與深度遍歷在 JS 中的應用,代碼可在 codepen 中查看。


免責聲明!

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



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