前言
我們首次接觸廣度優先搜索和深度優先搜索時,應該是在數據結構課上講的 “圖的遍歷”。還有就是刷題的時候,遍歷二叉樹/拓撲排序我們會經常用到這兩種遍歷方法。
廣度優先搜索算法(Breadth-First-Search,縮寫爲 BFS),是一種利用隊列實現的搜索算法。簡單來說,其搜索過程和 “湖面丟進一塊石頭激起層層漣漪” 類似。
深度優先搜索算法(Depth-First-Search,縮寫爲 DFS),是一種利用遞歸實現的搜索算法。簡單來說,其搜索過程和 “不撞南牆不回頭” 類似。
BFS 的重點在於隊列,而 DFS 的重點在於遞歸。這是它們的本質區別。
1. 廣度優先搜索法
廣度優先搜索,也叫做廣度優先遍歷,其主要思想類似於樹的層序遍歷。
- 從任意一個節點A開始,遍歷它的全部的鄰接點B,C
- 然後再以它其中一個鄰接點B爲起點,遍歷B的所有的鄰接點D,F。
- 然後再以它另外一個鄰接點C爲起點,遍歷C的所有的鄰接點G,H。
- 然後再以它其中一個鄰接點D爲起點,遍歷D的所有的鄰接點...。
- 以此類推....
僞代碼如下:
//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算法)。
1.1 無向圖的廣度優先遍歷
我們先給出一個圖:
- 先找到A,這是第一層。
- 再找到A的鄰接點,遍歷到B,C,D,F。
- 再找到B,C,D,F的鄰接點,遍歷到G,E,H
1.2 有向圖的廣度優先遍歷
思路和與無向圖類似,只不過需要考慮邊的走向問題。
- 先找到A,這是第一層。
- 再找到A的鄰接點,遍歷到B,C,F。
- 再找到B,C,F的鄰接點,遍歷到D,H
- 再找到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);
}
}
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