【圖論】廣度/深度優先搜索算法

前言

我們首次接觸廣度優先搜索深度優先搜索時,應該是在數據結構課上講的 “圖的遍歷”。還有就是刷題的時候,遍歷二叉樹/拓撲排序我們會經常用到這兩種遍歷方法。

廣度優先搜索算法(Breadth-First-Search,縮寫爲 BFS),是一種利用隊列實現的搜索算法。簡單來說,其搜索過程和 “湖面丟進一塊石頭激起層層漣漪” 類似。

深度優先搜索算法(Depth-First-Search,縮寫爲 DFS),是一種利用遞歸實現的搜索算法。簡單來說,其搜索過程和 “不撞南牆不回頭” 類似。

BFS 的重點在於隊列,而 DFS 的重點在於遞歸。這是它們的本質區別。

1. 廣度優先搜索法

廣度優先搜索,也叫做廣度優先遍歷,其主要思想類似於樹的層序遍歷。

  1. 從任意一個節點A開始,遍歷它的全部的鄰接點B,C
  2. 然後再以它其中一個鄰接點B爲起點,遍歷B的所有的鄰接點D,F。
  3. 然後再以它另外一個鄰接點C爲起點,遍歷C的所有的鄰接點G,H。
  4. 然後再以它其中一個鄰接點D爲起點,遍歷D的所有的鄰接點...。
  5. 以此類推....

僞代碼如下:

//queue:結果集,queue隊列
//nodeA:開始節點
public void bfsSearch(List<Node> queue ,Node StrartNode){
    //先選擇一個出發點,加入隊列。
    queue.add(StrartNode);
    int size = queue.getSize();
    for(int index=0;index<size;index++){
        //BFS的重點在於隊列,它的思路就是沿着queue的添加順序,依次遍歷他們的鄰接點。
        for(Node node : queue.get(index).getNeighbor()){
            //沒被搜索過,那麼加入結果集
            if(!queue.contain(node)){
                queue.add(node);
                size = queue.getSize();
            }
        }
    }
}

public void main(){
    //定義一個結果集,queue隊列
    List<Node> queue = new LinkedList<Node>();
    //先選擇一個節點作爲開始節點。
    Node startNode=nodeA;
    bfsSearch(queue,startNode);
    while(queue.size()不等於節點總數){
        //說明可能因爲邊有向的問題,有些節點沒有被遍歷到
        //此時需要在剩下的節點中另找一個出發點(假設爲B)
        startNode = nodeB;//省略找出B的過程
        bfsSearch(queue,startNode);
    }
}


BFS比較適合判斷二分圖,以及用於實現尋找最小生成樹(MST),如在BFS基礎上的Kruskal算法。還有尋找最短路徑問題(如Dijkstra算法)。

image

1.1 無向圖的廣度優先遍歷

我們先給出一個圖:

  1. 先找到A,這是第一層。
  2. 再找到A的鄰接點,遍歷到B,C,D,F。
  3. 再找到B,C,D,F的鄰接點,遍歷到G,E,H

1.2 有向圖的廣度優先遍歷

思路和與無向圖類似,只不過需要考慮邊的走向問題。

  1. 先找到A,這是第一層。
  2. 再找到A的鄰接點,遍歷到B,C,F。
  3. 再找到B,C,F的鄰接點,遍歷到D,H
  4. 再找到D,H的鄰接點,遍歷到E,G

2. 深度優先搜索法

深度優先搜索,也叫做深度優先遍歷,其主要思想是回溯法,它的核心是使用遞歸。

例如這張圖,從1開始到2,之後到5,5不能再走了,退回2,到6,退回2退回1,到3,以此類推;

僞代碼如下:

//stack:定義的結果集stack棧
//currentNode:本次遞歸搜索的當前node
public void dfsSearch(Stack<Node> stack,Node currentNode){
    if(currentNode沒有鄰接點 && !stack.contain(currentNode)){
        //壓入結果棧
        stack.push(currentNode);
        return;
    }
    //node有鄰接點,那麼遍歷鄰接點,依次深搜
    for(Node node : currentNode.getNeighbor()){
        if(node.isVisited() || stack.contain(currentNode)){
            continue;
        }
        node.setVisited(true);
        dfsSearch(stack,node);
        node.setVisited(false);
    }
    //currentNode的鄰接點都已經遍歷過了,現在邏輯回溯回currentNode
    //那麼需要將currentNode壓入結果棧
    stack.push(currentNode);
}

public void main(){
    //定義一個結果集
    Stack<Node> stack = new Stack<Node>();
    //先選擇一個節點作爲開始節點。
    Node startNode=nodeA;
    dfsSearch(stack,startNode);
    while(queue.size()不等於節點總數){
        //說明可能因爲邊有向的問題,有些節點沒有被遍歷到
        //此時需要在剩下的節點中另找一個出發點(假設爲B)
        startNode = nodeB;//省略找出B的過程
        dfsSearch(stack,startNode);
    }
}

image

2.1 無向圖的深度優先遍歷

我們先給出一個圖:

對上無向圖進行深度優先遍歷,從A開始:

第1步:訪問A。

第2步:訪問B(A的鄰接點)。 在第1步訪問A之後,接下來應該訪問的是A的鄰接點,即"B,D,F"中的一個。但在本文的實現中,頂點ABCDEFGH是按照順序存儲,B在"D和F"的前面,因此,先訪問B。

第3步:訪問G(B的鄰接點)。 和B相連只有"G"(A已經訪問過了)  

第4步:訪問E(G的鄰接點)。 在第3步訪問了B的鄰接點G之後,接下來應該訪問G的鄰接點,即"E和H"中一個(B已經被訪問過,就不算在內)。而由於E在H之前,先訪問E。

第5步:訪問C(E的鄰接點)。 和E相連只有"C"(G已經訪問過了)。

第6步:訪問D(C的鄰接點)。 

第7步:訪問H。因爲D沒有未被訪問的鄰接點;因此,一直回溯到訪問G的另一個鄰接點H。

第8步:訪問(H的鄰接點)F。

因此訪問順序是:A -> B -> G -> E -> C -> D -> H -> F

2.2 有向圖的深度優先遍歷

對上有向圖進行深度優先遍歷,從A開始:

第1步:訪問A。

第2步:訪問(A的出度對應的字母)B。 在第1步訪問A之後,接下來應該訪問的是A的出度對應字母,即"B,C,F"中的一個。但在本文的實現中,頂點ABCDEFGH是按照順序存儲,B在"C和F"的前面,因此,先訪問B。

第3步:訪問(B的出度對應的字母)F。 B的出度對應字母只有F。 

第4步:訪問H(F的出度對應的字母)。 F的出度對應字母只有H。 

第5步:訪問(H的出度對應的字母)G。

第6步:訪問(G的出度對應字母)E。 在第5步訪問G之後,接下來應該訪問的是G的出度對應字母,即"B,C,E"中的一個。但在本文的實現中,頂點B已經訪問了,由於C在E前面,所以先訪問C。

第7步:訪問(C的出度對應的字母)D。

第8步:訪問(C的出度對應字母)D。 在第7步訪問C之後,接下來應該訪問的是C的出度對應字母,即"B,D"中的一個。但在本文的實現中,頂點B已經訪問了,所以訪問D。

第9步:訪問E。D無出度,所以一直回溯到G對應的另一個出度E。

因此訪問順序是:A -> B -> F -> H -> G -> C -> D -> E

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章