js版本的BFS&DFS

0. 前言

廣度優先搜索(BFS)和深度優先搜索(DFS),你們可能在oj上見過,各類求路徑、最短路徑、最優方法、組合等等。因而,咱們不妨動手試一下js版本怎麼玩。javascript

1.隊列、棧

隊列是先進先出,後進後出,經常使用的操做是取第一個元素(shift)、尾部加入一個元素(push)。java

棧是後進先出,就像一個垃圾桶,後入的垃圾先被倒出來。經常使用的操做是,尾部加入元素(push),尾部取出元素(pop)node

2.BFS

BFS是靠一個隊列來輔助運行的。顧名思義,廣度搜索,就是對於一個樹形結構,咱們一層層節點去尋找目標節點。
image
按照這個順序進行廣度優先遍歷,明顯是隊列能夠完美配合整個過程:算法

  1. 1進隊列 【1】
  2. 取出隊列第一個元素1,將1的子節點234按順序加入隊列後面 【2,3,4】
  3. 取出隊首元素2,將他的子節點按順序加入隊列 【3,4,5,6】
  4. 取出3,將子節點7加入 【4,5,6,7】
  5. 取出4,將子節點89加入【5,6,7,8,9】
  6. 取出5,沒有子節點,沒有什麼幹
  7. 繼續一個個取出

到了最後,隊列清空,樹也遍歷了一次數組

1.1 矩陣形式的圖的遍歷

假設有幾個點,咱們須要設計一個算法,斷定兩個點有沒有相通測試

假設點12345是這樣的結構:
imagespa

問:1能不能到達5設計

顯然咱們一眼看上去是不會到達的,若是是設計算法的話,怎麼作呢?調試

咱們把點之間的關係用一個矩陣表示,0表示不鏈接,n行m列的1表示點n通向點mcode

var map = [ 
[0,1,1,0,0],
[0,0,1,1,0],
[0,1,1,1,0],
[1,0,0,0,0],
[0,0,1,1,0]
]
function bfs(arr,start,end){
    var row = arr.length
    var quene = []
    var i = start
    var visited = {}//記錄遍歷順序
    var order = [] //記錄順序,給本身看的
    quene.push(i) //先把根節點加入
    while(quene.length){ //若是隊列沒有被清空,也就是還沒遍歷完畢
        for(var j = 0;j<row;j++){
            if(arr[i][j]){ //若是是1
                if(!visited[j]){
                    quene.push(j)//隊列加入未訪問
                }
            }
        }
        quene.shift()//取出隊列第一個
        visited[i] = true//記錄已經訪問
        while(visited[quene[0]]){
            quene.shift()
        }
        order.push(i)//記錄順序
        i = quene[0]
    }
    return {visited:visited,result:!!visited[end],order:order}
}
bfs(map,0,4)

1.2 樹的BFS舉例

舉個例子,3月24號今日頭條筆試題第二題的最少操做步數:

定義兩個字符串變量:s和m,再定義兩種操做,
第一種操做:
m = s;
s = s + s;
第二種操做:
s = s + m;
假設s, m初始化以下:
s = "a";
m = s;
求最小的操做步驟數,能夠將s拼接到長度等於n
輸入一個整數n,代表咱們須要獲得s字符長度,0<n<10000
案例:
in:
6
out:
3

思路:利用廣度優先搜索,假設左節點是操做1,右節點是操做2,這樣子就造成了操做樹。利用bfs的規則,把上層的父節點按順序加入隊列,而後從前面按順序移除,同時在隊列尾部加上移除的父節點的子節點。我這裏,先把父節點拿出來對比,他的子節點放在temp,對比完了再把子節點追加上去

image
每一個節點分別用兩個數記錄s,m。發現第一次兩種操做是同樣的,因此我就略去右邊的了

function bfs(n){
    if(n<2||n!==parseInt(n)||typeof n !=='number') return
    if(n==2) return 1
    var quene = [[2,1]]//從2開始
    var temp = []//存放父節點隊列的子節點
    var count = 0
    var state = false//判斷是否結束循環
    while(!state){
        while(quene.length){//若是隊列不是空,從前面一個個取,並把他的子節點放在temp
            var arr = quene.pop()
            if(arr[0]==n){//找到了直接結束
                state = true
                break
            }
                temp.push([arr[0]*2,arr[1]*2])
                temp.push([arr[0]+arr[1],arr[1]])
        }
        count++//隊列已經空,說明這層的節點已經所有檢索完,並且子節點也保存好了
        quene = [...temp]//隊列是子節點全部的元素集合,重複前面操做
        temp = []
    }
    return count
}

3.DFS

DFS着重於這個搜索的過程,通常以「染色」的形式配合棧來運行,也比較完全。V8老生代的垃圾回收機制中的標記-清除也利用了DFS。咱們定義三種顏色:黑白灰,白色是未處理過的,灰是已經通過了但沒有處理,黑色是已經處理過了
仍是前面那幅圖
image

咱們用兩個數組,一個是棧,一個是保存咱們遍歷順序的,數組的元素拿到的都是原對象樹的引用,是會改變原對象的節點顏色的

  1. 從根節點開始,把根節點1壓入棧,染成灰色 【1:灰】
  2. 發現1的白色子節點2,壓入棧染色【1:灰,2:灰】
  3. 發現2的白色子節點5,入棧染色【1:灰,2:灰,5:灰】
  4. 發現5沒有白色子節點,因而5已經確認是遍歷過的,5出棧染黑色【1:灰,2:灰】,【5:黑】
  5. 回溯2,發現2還有白色子節點6,6入棧染灰,發現6沒有子節點,6出棧染黑色,【1:灰,2:灰】,【5:黑,6:黑】;又發現2沒有白色子節點,2出棧染黑色【1:灰】,【5:黑,6:黑,2:黑】
  6. 2又回溯1,發現1還有白色子節點3,3入棧染灰【1:灰,3:灰】,【5:黑,6:黑,2:黑】
  7. 一樣的,7沒有白色子節點,7入棧直接出棧染黑,【1:灰,3:灰】,【5:黑,6:黑,2:黑,7:黑】;3沒有白色子節點【1:灰】出棧染黑,【5:黑,6:黑,2:黑,7:黑,3:黑】
  8. 到了4,【1:灰,4:灰】,他有白色子節點89,而89沒有白色子節點,因此89入棧又直接出棧了【1:灰,4:灰】,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑】
  9. 4此次就沒有白色子節點了,到他出棧染黑,【1:灰】,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑,4:黑】
  10. 回溯,發現1沒有白色子節點,最後1出棧染黑,【5:黑,6:黑,2:黑,7:黑,3:黑,8:黑,9:黑,4:黑,1:黑】

咱們能夠看到,入棧的時候,從白色染灰色,出棧的時候,從灰色到黑色。整個過程當中,染黑的順序相似於二叉樹的後序遍歷

v8的垃圾回收,將持有引用的變量留下,沒有引用的變量清除。由於若是持有引用,他們必然在全局的樹中被遍歷到。若是沒有引用,那這個變量必然永遠是白色,就會被清理

咱們用對象來表示上面這棵樹:

var tree = {
    val: 1,
    children: [
        {val: 2,children: [{val:5,children:null,color:'white'},{val: 6,children:null,color:'white'}],color:'white'},
        {val: 3,children: [{val: 7,children:null,color:'white'}],color:'white'},
        {val: 4,children: [{val:8,children:null,color:'white'},{val: 9,children:null,color:'white'}],color:'white'}
    ],
    color: 'white'
}

開始咱們的DFS:

function dfs ( tree ) {
    var stack = []//記錄棧
    var order = []//記錄遍歷順序
    !function travel (node) {
        stack.push(node)//入棧
        node.color = 'gray'
        console.log(node)
        if(!node.children) {//沒有子節點的說明已經遍歷到底
            node.color = 'black'
            console.log(node)
            stack.pop()
            order.push(node)
            return
        }
        var children = node.children    
        children.forEach(child=>{
            travel(child)
        })
        node.color = 'black'
        stack.pop()//出棧
        order.push(node)
        console.log(node)
    }(tree)
    return order
}

過程用遞歸比較簡單,上面大部分代碼都是調試代碼,本身能夠改一下測試其餘的相似場景。遍歷完成後,tree上面每個節點都是黑色了。遍歷中間過程,每個節點入棧的時候是灰色的,出棧的時候是黑色的。

相關文章
相關標籤/搜索