算法:深度优先遍历和广度优先遍历
什么是深度、廣度優先遍歷
圖的遍歷是指,從給定圖中任意指定的頂點(稱為初始點)出發,按照某種搜索方法沿著圖的邊訪問圖中的所有頂點,使每個頂點僅被訪問一次,這個過程稱為圖的遍歷。遍歷過程中得到的頂點序列稱為圖遍歷序列。
圖的遍歷過程中,根據搜索方法的不同,又可以劃分為兩種搜索策略:
- 深度優先搜索(DFS,Depth First Search)
- 廣度優先搜索(BFS,Breadth First Search)
實現深度優先遍歷的關鍵在于回溯,實現廣度優先遍歷的關鍵在于回放。
深度優先搜索
深度優先搜索(Depth-First-Search),簡稱 DFS。最直觀的例子就是“走迷宮”。假設你站在迷宮的某個岔路口,然后想找到出口。你隨意選擇一個岔路口來走,走著走著發現走不通的時候,你就回退到上一個岔路口,重新選擇一條路繼續走,直到最終找到出口。這種走法就是一種深度優先搜索策略。
那么,如何在圖中應用深度優先搜索呢?
算法思想
-
對于圖來說:
- 假設初始狀態是圖中所有頂點均未被訪問
- 從某個頂點出發,然后依次從它的各個未被訪問的鄰接點出發深度優先搜索遍歷圖,直至圖中所有和v有路徑相通的頂點都被訪問到。
- 若此時尚有其他頂點未被訪問到,則另選一個未被訪問的頂點作起始點,重復上述過程,直至圖中所有頂點都被訪問到為止。
-
實現深度優先遍歷的關鍵在于回溯。所謂“回溯”,就是自后往前,追溯曾經走過的路徑。
算法特點
-
深度優先搜索是一個遞歸的過程。
- 首先,選定一個出發點后進行遍歷,如果有鄰接的未被訪問過的節點則繼續前進。
- 若不能繼續前進,則回退一步再前進
- 若回退一步仍然不能前進,則連續回退至可以前進的位置為止。
- 重復此過程,直到所有與選定點相通的所有頂點都被遍歷。
-
深度優先搜索是遞歸過程,帶有回退操作,因此需要使用棧存儲訪問的路徑信息。當訪問到的當前頂點沒有可以前進的鄰接頂點時,需要進行出棧操作,將當前位置回退至出棧元素位置。
圖解過程
無向圖的深度優先遍歷
以下圖所示無向圖說明深度優先搜索遍歷過程。
實例一
假設我們從頂點A開始,遍歷過程中的每一步如下:
- 首先選取頂點A為起始點,輸出A頂點信息,而且將A入棧,并且標記A為已訪問頂點
- A的鄰接頂點有C、D、F,從中任意選取一個頂點前進。這里我們選取C為前進位置頂點。輸出C頂點信息,將C入棧,并標記C為已訪問頂點。當前位置指向頂點C
- 頂點C的鄰接頂點有A、D、B,此時A已經標記為已訪問頂點,因此不能繼續訪問。從B或者D中選取一個頂點前進,這里我們選取B頂點為前進位置頂點。輸出B頂點信息,將B入棧,標記B頂點為已訪問頂點。當前位置指向B
- 頂點B的鄰接頂點只有C、E,C已被標記,不能繼續訪問,因此選取E為前進位置頂點,輸出E頂點信息,將E入棧,標記E頂點,當前位置指向E。
- 頂點E的鄰接頂點均已被標記,此時無法繼續前進,則需要進行回退。將當前位置回退至頂點B,回退的同時將E出棧。
- 頂點B的鄰接頂點也均被標記,需要繼續回退,當前位置回退至C,回退同時將B出棧。
- 頂點C可以前進的頂點位置為D,則輸出D頂點信息,將D入棧,并標記D頂點。當前位置指向頂點D。
- 頂點D沒有前進的頂點位置,因此需要回退操作。將當前位置回退至頂點C,回退同時將D出棧。
- 頂點C沒有前進的頂點位置,繼續回退,將當前位置回退至頂點A,回退同時將C出棧。
- 頂點A前進的頂點位置為F,輸出F頂點信息,將F入棧,并標記F。將當前位置指向頂點F。
- 頂點F的前進頂點位置為G,輸出G頂點信息,將G入棧,并標記G。將當前位置指向頂點G。
- 頂點G沒有前進頂點位置,回退至F。當前位置指向F,回退同時將G出棧。
- 頂點F沒有前進頂點位置,回退至A,當前位置指向A,回退同時將F出棧。
- 頂點A沒有前進頂點位置,繼續回退,棧為空,則以A為起始的遍歷結束。若圖中仍有未被訪問的頂點,則選取未訪問的頂點為起始點,繼續執行此過程。直至所有頂點均被訪問。
- 采用深度優先搜索遍歷順序為A->C->B->E->D->F->G。
利用一個臨時棧來實現回溯,最終遍歷完所有頂點
問題:
(1)必須選取A作為遍歷的起點嗎?
- 不是原則我們可以選取任何一個節點作為起點進行開始,進行深度優先遍歷
(2)當有多個鄰接點未被訪問時,可以選取哪個作為下一個起點呢?
- 隨便哪個都行。
- 當有多個臨界點可選時,相當于走迷宮時出現了多個分叉路口,我們只要不走之前走過的路就行了。所以關鍵在于標記哪個點是否已經走過。不過,一般我們會定義一個原則,必須不碰重復點的情況下,選擇走左/右手第一條沒有走過的路,這樣比較好理解
兩個原則:
- 右手原則: 在沒有碰到重復頂點的情況下,分叉路口始終是向右手邊走,每路過一個頂點就做一個記號
- 左手原則: 在沒有碰到重復頂點的情況下,分叉路口始終是向左手邊走,每路過一個頂點就做一個記號
下面以右手原則進行深度優先遍歷再看個例子
實例二
原則我們可以選取任何一個節點作為起點進行開始,進行深度優先遍歷,假設我們從頂點A開始,遍歷過程中的每一步如下:
-
第一步:從頂點A開始,將頂點A標記為已訪問節點
-
第二步:根據右手原則,訪問頂點B,并將B標記為已訪問節點
-
第三步:右手原則,訪問頂點C
-
第四步:右手原則,訪問頂點D
-
第五步:右手原則,訪問頂點E
-
第六步:右手原則,訪問頂點F
-
第七步:右手原則,應該先訪問頂點F的鄰接頂點A,但發現A已經被訪問,則訪問A之外的最右側頂點G
-
第八步:右手原則,先訪問頂點B,頂點B已經被訪問;在訪問頂點D,頂點D已經被訪問;最后訪問頂點H
-
第九步:發現頂點H的鄰接頂點均已被訪問,則退回到頂點G;
-
第十步:頂點G的鄰接頂點均已被訪問,則退回到頂點F;
-
第十一步:頂點F的鄰接頂點已被訪問,則退回到頂點E;
-
第十二步:頂點E的鄰接頂點均已被訪問,則退回到頂點D;
-
第十三步:頂點D的鄰接頂點I尚未被訪問,則訪問頂點I;
-
第十四步:頂點I的鄰接頂點均已被訪問,則退回到頂點D;
-
第十五步:頂點D的鄰接頂點均已被訪問,退回到頂點C;
-
第十六步:頂點C的鄰接頂點均已被訪問,則退回到頂點B;
-
頂點B的鄰接頂點均已被訪問,則退回到頂點A,頂點A為起始頂點,深度優先搜索結束。
圖的深度優先搜索和二叉樹的前序遍歷、中序遍歷、后序遍歷本質上均屬于一類方法。
上面的過程可以總結為以下3個步驟:
首先選定一個未被訪問過的頂點V作為起始頂點(或者訪問指定的起始頂點V),并將其標記為已訪問
然后搜索與頂點V鄰接的所有頂點,判斷這些頂點是否被訪問過,如果有未被訪問過的頂點W;再選取與頂點W鄰接的未被訪問過的一個頂點并進行訪問,依次重復進行。當一個頂點的所有的鄰接頂點都被訪問過時,則依次回退到最近被訪問的頂點。若該頂點還有其他鄰接頂點未被訪問,則從這些未被訪問的頂點中取出一個并重復上述過程,直到與起始頂點V相鄰接的所有頂點都被訪問過為止。
若此時圖中依然有頂點未被訪問,則再選取其中一個頂點作為起始頂點并進行遍歷,轉(2)。反之,則遍歷結束。
有向圖深度優先搜索
(1)以頂點A為起始點,輸出A,將A入棧,并標記A為已經訪問。當前位置指向A。
(2)以A為尾的邊只有1條,且邊的頭為頂點B,則前進位置為頂點B,輸出B,將B入棧,標記B。當前位置指向B。
(3)頂點B可以前進的位置有C與F,選取F為前進位置,輸出F,將F入棧,并標記F。當前位置指向F。
(4)頂點F的前進位置為G,輸出G,將G入棧,并標記G。當前位置指向G。
(5)頂點G沒有可以前進的位置,則回退至F,將G出棧。當前位置指向F。
(6)頂點F沒有可以前進的位置,繼續回退至B,將F出棧。當前位置指向B。
(7)頂點B可以前進位置為C和E,選取E,輸出E,將E入棧,并標記E。當前位置指向E。
(8)頂點E的前進位置為D,輸出D,將D入棧,并標記D。當前位置指向D。
(9)頂點D的前進位置為C,輸出C,將C入棧,并標記C。當前位置指向C。
(10)頂點C沒有前進位置,進行回退至D,回退同時將C出棧。
(11)繼續執行此過程,直至棧為空,以A為起始點的遍歷過程結束。若圖中仍有未被訪問的頂點,則選取未訪問的頂點為起始點,繼續執行此過程。直至所有頂點均被訪問。
性能分析
當圖采用鄰接矩陣存儲時,由于矩陣元素個數為n2n^2n2,因此時間復雜度就是O(n2)O(n^2)O(n2)
當圖采用鄰接表存儲時,鄰接表中只是存儲了邊結點(e條邊,無向圖也只是2e個結點),加上表頭結點為n(也就是頂點個數),因此時間復雜度為O(n+e)。
廣度優先搜索
算法思想
- 思想:
- 從圖中某頂點v出發,在訪問了v之后依次訪問v的各個未曾訪問過的鄰接點
- 然后分別從這些鄰接點出發依次訪問它們的鄰接點,并使得“先被訪問的頂點的鄰接點先于后被訪問的頂點的鄰接點被訪問,直至圖中所有已被訪問的頂點的鄰接點都被訪問到。
- 如果此時圖中尚有頂點未被訪問,則需要另選一個未曾被訪問過的頂點作為新的起始點,重復上述過程,直至圖中所有頂點都被訪問到為止。
- 實現廣度優先遍歷的關鍵在于回放。
回溯與重放是完全相反的過程。
仍然以剛才的圖為例,按照廣度優先遍歷的思想
- 我們先遍歷頂點0,然后遍歷其鄰接點1、3
- 接下來我們要遍歷更外圍的頂點,可是如何找到這些更外圍的頂點呢?我們需要把剛才遍歷過的頂點1,3按照順序回顧一遍,從頂點1發現了鄰接點2,從頂點3發現了鄰接點4。于是得到了順序2,4
- 再把剛才遍歷過的頂點2,4按照順序回顧一遍,分別得到鄰接點5,6
- 再把剛才遍歷過的頂點5,7按照順序回顧一遍,分別得到鄰接點7,7。7只需要打印一次,所以我們需要一個東西來標記當前頂點是否已經訪問過
像這樣把遍歷過的頂點按照之前的遍歷順序重新回顧,就叫做重放。
- 同樣的,要實現重放也需要額外的存儲空間,可以利用隊列的先入先出特性來實現。
- 另外,還需要標記某個點是否已經被訪問過,可以用數組、set等來實現
可以看出,廣度優先搜索它其實就是一種“地毯式”層層推進的搜索策略,即先查找離起始頂點最近的,然后是次近的,依次往外搜索。
算法特點
廣度優先搜索類似于樹的層次遍歷,是按照一種由近及遠的方式訪問圖的頂點。在進行廣度優先搜索時需要使用隊列存儲頂點信息。
圖解過程
無向圖的廣度優先搜索
(1)選取A為起始點,輸出A,A入隊列,標記A,當前位置指向A。
(2)隊列頭為A,A出隊列。A的鄰接頂點有B、E,輸出B和E,并將B和E入隊,以及標記B、E為已訪問。當前位置指向B。
(3)隊列頭為B,B出隊列。B的鄰接頂點有C、D,輸出C、D,將C、D入隊列,并標記C、D。當前位置指向B。
(4)隊列頭為E,E出隊列。E的鄰接頂點有D、F,但是D已經被標記,因此輸出F,將F入隊列,并標記F。當前位置指向E。
(5)隊列頭為C,C出隊列。C的鄰接頂點有B、D,但B、D均被標記。無元素入隊列。當前位置指向C。
(6)隊列頭為D,D出隊列。D的鄰接頂點有B、C、E,但是B、C、E均被標記,無元素入隊列。當前位置指向D。
(7)隊列頭為F,F出隊列。F的鄰接頂點有G、H,輸出G、H,將G、H入隊列,并標記G、H。當前位置指向F。
(8)隊列頭為G,G出隊列。G的鄰接頂點有F,但F已被標記,無元素入隊列。當前位置指向G。
(9)隊列頭為H,H出隊列。H的鄰接頂點有F,但F已被標記,無元素入隊列。當前位置指向H。
(10)隊列空,則以A為起始點的遍歷結束。若圖中仍有未被訪問的頂點,則選取未訪問的頂點為起始點,繼續執行此過程。直至所有頂點均被訪問。
有向圖的廣度優先搜索
(1)選取A為起始點,輸出A,將A入隊列,標記A。
(2)隊列頭為A,A出隊列。以A為尾的邊有兩條,對應的頭分別為B、C,則A的鄰接頂點有B、C。輸出B、C,將B、C入隊列,并標記B、C。
(3)隊列頭為B,B出隊列。B的鄰接頂點為C,C已經被標記,因此無新元素入隊列。
(4)隊列頭為C,C出隊列。C的鄰接頂點有E、F。輸出E、F,將E、F入隊列,并標記E、F。
(5)列頭為E,E出隊列。E的鄰接頂點有G、H。輸出G、H,將G、H入隊列,并標記G、H。
(6)隊列頭為F,F出隊列。F無鄰接頂點
(7)隊列頭為G,G出隊列。G無鄰接頂點
(8)隊列頭為H,H出隊列。H鄰接頂點為E,但是E已被標記,無新元素入隊列
(9)隊列為空,以A為起始點的遍歷過程結束,此時圖中仍有D未被訪問,則以D為起始點繼續遍歷。選取D為起始點,輸出D,將D入隊列,標記D
(10)隊列頭為D,D出隊列,D的鄰接頂點為B,B已被標記,無新元素入隊列
(11)隊列為空,且所有元素均被訪問,廣度優先搜索遍歷過程結束。廣度優先搜索的輸出序列為:A->B–>C->E->F->G->H->D。
算法分析
我們來看下,廣度優先搜索的時間、空間復雜度是多少呢?假設圖有V個頂點,E條邊
- 每個頂點都需要進出一遍隊列,每個邊都會被訪問一次。所以,廣度優先搜索的時間復雜度是O(V+E)。當然,對于一個連通圖來說,也就是說一個圖中的所有頂點都是連通的,,E 肯定要大于等于 V-1,所以,廣度優先搜索的時間復雜度也可以簡寫為 O(E)
- 廣度優先搜索的空間消耗主要在幾個輔助變量 visited 數組、queue 隊列上。這兩個存儲空間的大小都不會超過頂點的個數,所以空間復雜度是 O(V)。
總結
圖的遍歷主要就是這兩種遍歷思想,深度優先搜索使用遞歸方式,需要棧結構輔助實現。廣度優先搜索需要使用隊列結構輔助實現。在遍歷過程中可以看出,
- 對于連通圖,從圖的任意一個頂點開始深度或廣度優先遍歷一定可以訪問圖中的所有頂點
- 對于非連通圖,從圖的任意一個頂點開始深度或廣度優先遍歷并不能訪問圖中的所有頂點。
實現
深度優先遍歷
當圖采用鄰接矩陣進行存儲,遞歸的實現操作:
#define MAXVBA 100 #define INFINITY 65536typedef struct {char vexs[MAXVBA];int arc[MAXVBA][MAXVBA];int numVertexes, numEdges; } MGraph;// 鄰接矩陣的深度有限遞歸算法#define TRUE 1 #define FALSE 0 #define MAX 256typedef int Boolean; // 這里我們定義Boolean為布爾類型,其值為TRUE或FALSE Boolean visited[MAX]; // 訪問標志的數組void DFS(MGraph G, int i){visited[i] = TRUE;printf("%c", G.vexs[i]);for (int j = 0; j < G.numVertexes; ++j) {if (G.arc[i][j] == 1 && !visited[j]){DFS(G, j); // 對為訪問的鄰接頂點遞歸調用}} }// 鄰接矩陣的深度遍歷操作 void DFSTraverse(MGraph G){int i;// 初始化所有頂點狀態都是未訪問過狀態for (i = 0; i < G.numVertexes; ++i) {visited[i] = FALSE;}//防止圖為非聯通的情況,遍歷整個圖for (i = 0; i < G.numVertexes; ++i) {if (!visited[i]){ // 若是連通圖,只會執行一次DFS(G, i);}} }當圖采用鄰接矩陣進行存儲,棧的實現操作:
void DFS_Stack(MGraph G, int i) {int node;int count = 1;printf("%c ", G.vexs[i]); // 打印已訪問頂點visited[i] = TRUE;node = i;push(i); //開始的節點入棧while(count < G.numVertexes) //still has node not visited{/* 所有被訪問的節點依次入棧,只有node當找不到下一個相連的節點時,才使用出棧節點 */for(j=0; j < G.numVertexes; j++){if(G.arc[node][j] == 1 && visited[j] == FALSE){visited[j] = TRUE;printf("%c ", G.vexs[j]);count++;push(j); //push node jbreak;}}if(j == G.numVertexes) //與node相連的頂點均已被訪問過,所以需要從stack里取出node的上一個頂點,再看該頂點的鄰接頂點是否未被訪問node = pop();else //找到與node相連并且未被訪問的頂點,node = j;} }
鄰接表存儲下圖的深度優先搜索代碼實現,與鄰接矩陣的思想相同,只是實現略有不同:
廣度優先遍歷
// 鄰接矩陣的廣度遍歷算法 void BFSTraverse(MGraph G) {int i, j;Queue Q;for( i=0; i < G.numVertexes; i++ ){visited[i] = FALSE;}initQueue( &Q );for( i=0; i < G.numVertexes; i++ ){if( !visited[i] ){printf("%c ", G.vex[i]);visited[i] = TRUE;EnQueue(&Q, i);while( !QueueEmpty(Q) ){DeQueue(&Q, &i);for( j=0; j < G.numVertexes; j++ ){if( G.art[i][j]==1 && !visited[j] ){printf("%c ", G.vex[j]);visited[j] = TRUE;EnQueue(&Q, j);}}}}} }總結
以上是生活随笔為你收集整理的算法:深度优先遍历和广度优先遍历的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue使用echarts来绘制中国地图下
- 下一篇: XP下网络连接提示受限制的解决方法