從圖中某一頂點出發訪遍圖中其余頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷。根據遍歷路徑的不同,通常有兩種遍歷圖的方法:深度優先遍歷和廣度優先遍歷。它們對無向圖和有向圖都適用。圖的遍歷算法是求解圖的連通性問題、拓撲排序和求關鍵路徑等算法的基礎。
圖的遍歷算法設計需要考慮三個問題:
(1)圖的特點是沒有首尾之分,所以算法的參數要指定訪問的第一個頂點。
(2)對圖的遍歷路徑有可能構成一個回路,從而造成死循環,所以算法設計要考慮遍歷路徑可能出現的死循環問題。
(3)一個頂點可能和若干個頂點都是鄰接頂點,要使一個頂點的所有鄰接頂點按照某種次序被訪問。
7.3.1 深度優先搜索遍歷
深度優先遍歷的思想類似於樹的先序遍歷。其遍歷過程可以描述為:從圖中某個頂點v出發,訪問該頂點,然后依次從v的未被訪問的鄰接點出發繼續深度優先遍歷圖中的其余頂點,直至圖中所有與v有路徑相通的頂點都被訪問完為止。
假設給定圖G的初始狀態是所有頂點均未曾訪問過,在G中任選一頂點vi 為初始出發點,則深度優先遍歷可定義如下:首先訪問出發點,並將其標記為已訪問過,然后,依次從vi 出發遍歷vi 的每一個鄰接點,若vj未曾訪問過,則以vj 為新的出發點繼續進行深度優先遍歷,直至圖中所有和vi有路徑相通的頂點都被訪問到為止。因此,若G是連通圖,則從初始出發點開始的遍歷過程結束,也就意味着完成了對圖G的遍歷。
對於圖7-6所示的無向連通圖,若頂點v0為初始訪問的頂點,則深度優先遍歷頂點訪問順序是:v0→v1→v2→v5→v4→v3。
連通圖的深度優先遍歷遞歸算法可描述為:
(1)訪問頂點vi並標記頂點vi為已訪問;
(2)查找頂點v的第一個鄰接頂點vj;
(3)若頂點v的鄰接頂點vj存在,則繼續執行,否則算法結束;
(4)若頂點vj尚未被訪問,則深度優先遍歷遞歸訪問頂點vj;
(5)查找頂點vi的鄰接頂點vj的下一個鄰接頂點,轉到步驟(3)。
當尋找頂點vi的鄰接頂點vj成功時繼續進行,當尋找頂點vi的鄰接頂點vj失敗時回溯到上一次遞歸調用的地方繼續進行。為了在遍歷過程中便於區分頂點是否被訪問,需附設訪問標志數組visited[ ],其初值為0,一旦某個頂點被訪問,則其相應的分量置為1。
以鄰接矩陣和鄰接表作為圖的存儲結構給出深度優先遍歷的遞歸算法如下:
DFS1:圖是按照鄰接矩陣的方式存儲
DFS2:圖是按照鄰接表的方式存儲
這兩種數據結構的的DFS遍歷的時間和空間復雜度不同
【算法7.3】
void DFS1 (MGraph MG, int i)
{ int j;
visited[i]=1;
printf("%3c",MG.vexs[i]);
for(j=1; j<=MG.vex_num; j++)
if(!visited[j]&&MG.arcs[i][j]==1)
DFS1 (MG, j);
}
void DFS2(AdjList G,int i)
{ int j;
EdgeLinklist *p;
visited[i]=1;
printf("%3c",G.vertices[i].Elem);
for(p=G.vertices[i].firstedge;p;p=p->next)
{j=p->adjvex;
if(!visited[j])
DFS2(G, j); }
}
算法分析:
遍歷圖的過程實質是對每個頂點搜索鄰接點的過程,具有n個頂點e條邊的連通圖,主要時間耗費在從該頂點出發遍歷它的所有鄰接點上。用鄰接矩陣表示圖時,遍歷一個頂點的所有鄰接點需花費O(n)時間來檢查矩陣相應行中所有的n個元素,故從n個頂點出發遍歷所需的時間為O(n2),即算法的時間復雜度為O(n2)。用鄰接表表示圖時,遍歷n個頂點的所有鄰接點即是對各邊表結點掃描一遍,故算法DFS2的時間復雜度為O(n+e)。算法DFS1和DFS2所用的輔助空間是標志數組和實現遞歸所用的棧,它們的空間復雜度為O(n)。
7.3.2 廣度優先搜索
圖的廣度優先遍歷算法是一個分層遍歷的過程,和樹的層序遍歷算法類同,是從圖的某一頂點V0出發,訪問此頂點后,依次訪問V0的各個未曾訪問過的鄰接點;然后分別從這些鄰接點出發,直至圖中所有已被訪問的頂點的鄰接點都被訪問到;若此時圖中尚有頂點未被訪問,則另選圖中一個未被訪問的頂點作起點,重復上述過程,直至圖中所有頂點都被訪問為止。
對於圖7-6所示的無向連通圖,若頂點v0為初始訪問的頂點,則廣度優先遍歷頂點訪問順序是:v0→v1→v3→v2→v4→v5。遍歷過程如圖7-7所示
圖7-7 廣度優先搜索遍歷過程
圖的廣度優先遍歷算法需要一個隊列以保持訪問過的頂點的順序,以便按順序訪問這些頂點鄰接頂點。連通圖的廣度優先遍歷算法為:
(1)訪問初始頂點v並標記頂點v為已訪問;
(2)頂點v入隊列;
(3)當隊列非空時則繼續執行,否則算法結束;
(4)出隊列取得隊頭頂點u;
(5)查找頂點u的第一個鄰接頂點w;
(6)若頂點u的鄰接頂點w不存在,則轉到步驟(3),否則執行后序語句:
①若頂點w尚未被訪問,則訪問頂點w並標記頂點w為已訪問;
②頂點w入隊列;
③查找頂點u的鄰接頂點w后的下一個鄰接頂點,轉到步驟(6)。
以鄰接矩陣為圖的存儲結構的廣度優先遍歷的非遞歸算法源代碼如下:
【算法7.4】
void BFS1(Mgraph G ,int k)
{ int i,j;
int visited[Vex_num];
SqQueue q;
initqueue(&q);
printf("visit vertex: V%d\n", G->vexs[k]);
visited[k]=1;
Enqueue(&q,k);
while(!QueueEmpty(&q))
{
i=Dequeue(&q);
for(j=0; j<G->n; j++)
{
printf("visit vertex :V%d \n",G->vexs[j]);
visited[j]=1;
Enqueue(&q,j);
}
}
}
以鄰接表為圖的存儲結構的廣度優先遍歷的非遞歸算法
【算法7.5】
void BFS2(int v)
/*v是表頭結點的下標*/
{ EdgeLinklist *ptr;
int v1, w;
printf(" %d \n", v); /* 輸出該頂點 */
visited[v] =1; /* 標志置為1*/
enqueue(v); /*將該頂點入隊尾 */
while((v1=dequeue()!=EOF)
{ /* 循環使屬於同一層頂點的相鄰頂點依次出隊*/
ptr=list[v1].firstedge; /*取出該頂點的第一個相鄰頂點地址 */
while(ptr!=NULL) /*循環依次訪問各相鄰頂點 */
{
w=ptr->adjtex; /* 取出該頂點的序號 */
ptr=ptr->next; /* 取出下一個相鄰頂點的地址以備訪問*/
if(visited [w]==0)
{
printf("%d \n", w);
visited[w]=1;
enqueue(w);
}
}
}
}
算法分析:對於具有n個頂點和e條邊的連通圖,因為每個頂點均入隊一次,(while語句)執行次數為n。算法BFS1的內循環(for語句)執行n次,故算法BFS1的時間復雜度為O(n2);算法BFS2的內循環(for語句)執行次數取決於各頂點的邊表結點個數,內循環執行的總次數是邊表結點的總個數2e,故算法BFS2的時間復雜度是O(n+e)。算法BFS1和BFS2所用的輔助空間是隊列和標志數組,故它們的空間復雜度為O(n))。
對於連通圖,從圖的任意一個頂點開始深度或廣度優先遍歷一定可以訪問圖中的所有頂點,但對於非連通圖,從圖的任意一個頂點開始深度或廣度優先遍歷並不能訪問圖中的所有頂點。對於非連通圖,從圖的任意一個頂點開始深度或廣度優先遍歷只能訪問和初始頂點連通的所有頂點。
兩種遍歷方式哈!