【資料結構與演算法】自己動手實現圖的BFS和DFS(附完整原始碼)

蘭亭風雨發表於2014-02-22

轉載請註明出處:http://blog.csdn.net/ns_code/article/details/19617187


圖的儲存結構

    本文的重點在於圖的深度優先搜尋(DFS)和廣度優先搜尋(BFS),因此不再對圖的基本概念做過多的介紹,但是要先大致瞭解下圖的幾種常見的儲存結構。

    鄰接矩陣

    鄰接矩陣既可以用來儲存無向圖,也可以用來儲存有向圖。該結構實際上就是用一個二維陣列(鄰接矩陣)來儲存頂點的資訊和頂點之間的關係(有向圖的弧或無向圖的邊)。其描述形式如下:

 //圖的鄰接矩陣儲存表示
 #define MAX_NUM 20 // 最大頂點個數
 enum GraphKind{GY,GN}; // {有向圖,無向圖}
 typedef struct
 {
    VRType adj; // 頂點關係型別。對無權圖,用1(是)或0(否)表示是否相鄰;對帶權圖,則為權值
    InfoType *info; // 與該弧或邊相關資訊的指標(可無)
 }ArcCell,AdjMatrix[MAX_NUM][MAX_NUM]; // 二維陣列
 typedef struct
 {
    VertexType vexs[MAX_NUM]; // 頂點向量
    AdjMatrix arcs; // 鄰接矩陣
    int vexnum,arcnum; // 圖的當前頂點數和弧(邊)數
    GraphKind kind; // 圖的種類標誌
 }Graph;
    我們分別看下面兩個圖,左邊為有向圖,右邊為無向圖


             
    上面兩個圖均為無權圖,我們假設儲存的時候,V0的序號為0,V1的序號為1,V2的序號為2。。。,且adj為1表示兩頂點間沒有沒有連線,為0時表示有連線。則有向圖的鄰接矩陣如下圖左邊的矩陣所示,無向圖的鄰接矩陣如下圖右邊的矩陣所示;
         
    根據鄰接矩陣很容易判斷圖中任意兩個頂點之間連通與否,並可以求出各個頂點的度。
    1、對於無向圖,觀察右邊的矩陣,發現頂點Vi的度即是鄰接矩陣中第i行(或第i列)的元素之和。
    2、對於有向圖,由於需要分別計算出度和入讀,觀察左邊的矩陣,發現頂點Vi的出度即為鄰接矩陣第i行元素之和,入度即為鄰接矩陣第i列元素之和,因此頂點Vi的度即為鄰接矩陣中第i行元素和第i列元素之和。
    很明顯,鄰接矩陣所佔用的儲存空間與圖的邊數或弧數無關,因此適用於邊數或弧數較多的稠密圖。

    鄰接表

    鄰接表是圖的一種鏈式儲存結構,既適合於儲存無向圖,也適合於儲存有向圖。在鄰接表中,用一個一維陣列儲存圖中的每個頂點的資訊,同時為每個頂點建立一個單連結串列,連結串列中的節點儲存依附在該頂點上的邊或弧的資訊。其描述形式如下
 //圖的鄰接表儲存表示
 #define MAX_NUM 20
 enum GraphKind{GY,GN}; // {有向圖,無向圖}
 typedef struct 
 {
    int adjvex; // 該弧所指向的頂點或邊的另一個頂點的位置
    ArcNode *nextarc; // 指向下一條弧或邊的指標
    InfoType *info; // 與弧或邊相關資訊的指標(可無)
 }ArcNode;// 表結點
 typedef struct
 {
    VertexType data; // 頂點資訊
    ArcNode *firstarc; // 第一個表結點的地址,指向第一條依附該頂點的弧或邊的指標
 }VNode,AdjList[MAX_NUM]; // 頭結點
 struct 
 {
    AdjList vertices;
    int vexnum,arcnum; // 圖的當前頂點數和弧(邊)數
    GraphKind kind; // 圖的種類標誌
 }Graph;
    依然以上面的有向圖和無向圖為例,採用鄰接表來儲存,可以得到對應的儲存形式如下:
    在鄰接表上容易找到任意一個頂點的第一個鄰接點和下一個鄰接點,但是要判定任意兩個頂點之間是否有邊或弧需搜尋兩個頂點對應的連結串列,不及鄰接矩陣方便。
    很明顯,鄰接表所佔用的儲存空間與圖的邊數或弧數有關,因此適用於邊數或弧數較少的稀疏圖。

    十字連結串列

    十字連結串列也是一種鏈式儲存結構,用來表示有向圖,它在有向圖鄰接表的基礎上加入了指向弧尾的指標。表示形式如下:
 //有向圖的十字連結串列儲存表示
 #define MAX_NUM 20
 typedef struct // 弧結點
 {
   int tailvex,headvex; // 該弧的尾和頭頂點的位置
   ArcBox *hlink,*tlink; // 分別為弧頭相同和弧尾相同的弧的鏈域
   InfoType *info; // 該弧相關資訊的指標,可指向權值或其他資訊(可無)
 }ArcBox;
 typedef struct // 頂點結點
 {
   VertexType data;
   ArcBox *firstin,*firstout; // 分別指向該頂點第一條入弧和出弧
 }VexNode;
 struct
 {
   VexNode xlist[MAX_NUM]; // 表頭向量(陣列)
   int vexnum,arcnum; // 有向圖的當前頂點數和弧數
 }Graph;    
    其思想也很容易理解,這裡不再細說。
    在十字連結串列中,既容易找到以某個頂點為尾的弧,也容易找到以某個頂點為頭的弧,因而容易求得頂點的出度和入度。

    鄰接多重表

    鄰接多重表也是一種鏈式儲存結構,用來表示無向圖,與有向圖的十字連結串列相似,這裡也不再細說,直接給出其表示形式:
//無向圖的鄰接多重表儲存表示
 #define MAX_NUM 20
 typedef struct 
 {
   VisitIf mark; // 訪問標記
   int ivex,jvex; // 該邊依附的兩個頂點的位置
   EBox *ilink,*jlink; // 分別指向依附這兩個頂點的下一條邊
   InfoType *info; // 該邊資訊指標,可指向權值或其他資訊
 }EBox;
 typedef struct
 {
   VertexType data;
   EBox *firstedge; // 指向第一條依附該頂點的邊
 }VexBox;
 typedef struct 
 {
   VexBox adjmulist[MAX_NUM];
   int vexnum,edgenum; // 無向圖的當前頂點數和邊數
 }Graph;

圖的遍歷

    圖的遍歷比樹的遍歷要複雜的多,因為圖的任一頂點都有可能與其他的頂點相鄰接,因此,我們需要設定一個輔助陣列visited[]來標記某個節點是否已經被訪問過。圖的遍歷通常有兩種方法:深度優先遍歷(BFS)和廣度優先遍歷(DFS),兩種遍歷方式的思想都不難理解。下面我們就以如下圖所示的例子來說明圖的這兩種遍歷方式的基本思想,並用鄰接表作為圖的儲存結構,給出BFS和DFS的實現程式碼(下圖為無向圖,有向圖的BFS和DFS實現程式碼與無向圖的相同):

    我們以鄰接表作為上圖的儲存結構,並將A、B、C、D、E、F、G、H在頂點陣列中的序號分別設為0、1、2、3、4、5、6、7。我們根據上圖所包含的資訊,精簡了鄰接表的儲存結構,採取如下所示的結構來儲存圖中頂點和邊的相關資訊:

#define NUM 8          //圖中頂點的個數

/*
用鄰接表作為圖的儲存結構
在鄰接表中,用一個一維陣列儲存圖中的每個頂點的資訊,
同時為每個頂點建立一個單連結串列,連結串列中的節點儲存依附在該頂點上的邊或弧的資訊
*/
typedef struct node
{	//連結串列中的每個節點,儲存依附在該節點上的邊或弧的資訊
	int vertex;          //在有向圖中表示該弧所指向的頂點(即弧頭)的位置,
				         //在無向圖中表示依附在該邊上的另一個頂點的位置
	struct node *pNext;  //指向下一條依附在該頂點上的弧或邊
}Node;
typedef struct head
{	//陣列中的每個元素,儲存圖中每個頂點的相關資訊
	char data;          //頂點的資料域
	Node *first;        //在有向圖中,指向以該頂點為弧尾的第一條弧
						//在無向圖中,指向依附在該頂點上的第一條邊
}Head,*Graph;           //動態分配陣列儲存每個頂點的相關資訊

    那麼用鄰接表來表示上圖中各頂點間的關係,如下圖所示:


    深度優先搜尋

    深度優先搜尋(DFS)類似於樹的先序遍歷(如尚未掌握圖的先序遍歷,請移步這裡:http://blog.csdn.net/ns_code/article/details/12977901)。其基本思想是:從圖中某個頂點v出發,遍歷該頂點,而後依次從v的未被訪問的鄰接點出發深度優先遍歷圖中,直到圖中所有與v連通的頂點都已被遍歷。如果當前圖只是需要遍歷的非連通圖的一個極大連通子圖,則另選其他子圖中的一個頂點,重複上述遍歷過程,直到該非連通圖中的所有頂點都被遍歷完。很明顯,這裡要用到遞迴的思想。

    結合上面的例子來分析,假設從A點開始遍歷該圖,根據圖中頂點的儲存關係,會按照下面的步驟來遍歷該圖:

    1、在訪問完頂點A後,選擇A的第一個鄰接點B進行訪問;

    2、而後看B的鄰接點,由於B的第一個鄰接點A已經被訪問過,故選擇其下一個鄰接點D進行訪問;

    3、而後看D的鄰接點,由於D的第一個鄰接點B已經被訪問過,故選擇其下一個鄰接點H進行訪問;

    4、而後看H的鄰接點,由於H的第一個鄰接點D已經被訪問過,故選擇其下一個鄰接點E進行訪問;

    5、而後看E的鄰接點,由於E的第一個鄰接點B已經被訪問過,那麼看其第二個鄰接點H,也被訪問過了,E的鄰接點已經全部被訪問過了。

    6、這時退回到上一層遞迴,回到頂點H,同樣H的鄰接點也都被訪問完畢,再退回到D,D的鄰接點也已訪問完畢,再退回到B,一直退回到A;

    7、由於A的下一個鄰接點C還沒有被訪問,因此訪問C;

    8、而後看C的鄰接點,由於C的第一個鄰接點A已經被訪問過,故選擇其下一個鄰接點F進行訪問;

    9、而後看F的鄰接點,由於F的第一個鄰接點C已經被訪問過,故選擇其下一個鄰接點G進行訪問;

    10、而後看G的鄰接點,由於G的臨界點都已被訪問完畢,因此會退到上一層遞迴,看F頂點;

    11、同理,由F再回退到C,再由C回退到A,這是第一層的遞迴,A的所有鄰接點也已都被訪問完畢,遍歷到此結束。

    綜上,上圖的DFS是按照如下順序進行的:

A->B->D->H->E->H->D->B->A->C->F->G->F->C->A

    其中,紅色部分代表首次訪問,在這部分頂點被訪問後,我們便將visited陣列的對應元素設為true,表明這些頂點已經被訪問過了,因此我們可以得到深度優先搜尋得到的順序為:

A->B->D->H->E->C->F->G

    深度優先搜尋的程式碼實現如下:

/*
從序號為begin的頂點出發,遞迴深度優先遍歷連通圖Gp
*/
void DFS(Graph Gp, int begin)
{
	//遍歷輸出序號為begin的頂點的資料域,並儲存遍歷資訊
	printf("%c ",Gp[begin].data);
	visited[begin] = true; 

	//迴圈訪問當前節點的所有鄰接點(即該節點對應的連結串列)
	int i;
	for(i=first_vertex(Gp,begin); i>=0; i=next_vertex(Gp,begin,i))
	{
		if(!visited[i])  //對於尚未遍歷的鄰接節點,遞迴呼叫DFS
			DFS(Gp,i);
	}
} 

/*
從序號為begin的節點開始深度優先遍歷圖Gp,Gp可以是連通圖也可以是非連通圖
*/
void DFS_traverse(Graph Gp,int begin)
{
	int i;
	for(i=0;i<NUM;i++)    //初始化遍歷標誌陣列
		visited[i] = false; 

	//先從序號為begin的頂點開始遍歷對應的連通圖
	DFS(Gp,begin);
	//如果是非連通圖,該迴圈保證每個極大連通子圖中的頂點都能被遍歷到   
	for(i=0;i<NUM;i++)
	{	    
		if(!visited[i])
			DFS(Gp,i);
	}
}
    這裡呼叫了first_vertex()和next_vertex()兩個函式,根據如上圖手繪的鄰接表的儲存結構,這兩個函式程式碼實現及詳細註釋如下:

/*
返回圖Gp中pos頂點(序號為pos的頂點)的第一個鄰接頂點的序號,如果不存在,則返回-1
*/
int first_vertex(Graph Gp,int pos)
{
	if(Gp[pos].first)  //如果存在鄰接頂點,返回第一個鄰接頂點的序號
		return 	Gp[pos].first->vertex;
	else              //如果不存在,則返回-1
		return -1;
}

/*
cur頂點是pos頂點(cur和pos均為頂點的序號)的其中一個鄰接頂點,
返回圖Gp中,pos頂點的(相對於cur頂點)下一個鄰接頂點的序號,如果不存在,則返回-1
*/
int next_vertex(Graph Gp,int pos,int cur)
{
	Node *p = Gp[pos].first; //p初始指向頂點的第一個鄰接點
	//迴圈pos節點對應的連結串列,直到p指向序號為cur的鄰接點
	while(p->vertex != cur)
		p = p->pNext;

	//返回下一個節點的序號
	if(p->pNext)
		return p->pNext->vertex; 
	else 
		return -1;
}

    廣度優先搜尋

    廣度優先搜尋(BFS)類似於樹的層序遍歷(如尚未掌握圖的先序遍歷,請移步這裡:http://blog.csdn.net/ns_code/article/details/13169703)。其基本思想是:從頭圖中某個頂點v出發,訪問v之後,依次訪問v的各個未被訪問的鄰接點,而後再分別從這些鄰接點出發,依次訪問它們的鄰接點,直到圖中所有與v連通的頂點都已被訪問。如果當前圖只是需要遍歷的非連通圖的一個極大連通子圖,則另選其他子圖中的一個頂點,重複上述遍歷過程,直到該非連通圖中的所有頂點都被遍歷完。很明顯,跟樹的層序遍歷一樣,圖的廣度優先搜尋要用到佇列來輔助實現。

    同樣以上面的例子來分析,假設從A點開始遍歷該圖,根據圖中頂點的儲存關係,會按照下面的步驟來遍歷該圖:

    1、在訪問完頂點A後,首先訪問A的第一個鄰接點B,而後訪問A的第二個鄰接點C;

    2、再根據順序訪問B的未被訪問的鄰接點,先訪問D,後訪問E;

    3、再根據順序訪問C的未被訪問的鄰接點,先訪問F,在訪問G;

    4、而後一次訪問D、E、F、G等頂點的未被訪問的鄰接點;

    5、D的鄰接點中,只有H未被訪問,因地訪問H;

    6、E、F、G、H等都沒有未被訪問的鄰接點了,遍歷到此結束。

    綜上,我們可以得到廣度優先搜尋得到的順序為:

A->B->C->D->E->F->G->H

    深度優先搜尋的程式碼實現如下:

/*
從序號為begin的頂點開始,廣度優先遍歷圖Gp,Gp可以是連通圖也可以是非連通圖
*/
void BFS_traverse(Graph Gp,int begin)
{
	int i;
	for(i=0;i<NUM;i++)    //初始化遍歷標誌陣列
		visited[i] = false;

	//先從序號為begin的頂點開始遍歷對應的連通圖
	BFS(Gp,begin);
	//如果是非連通圖,該迴圈保證每個極大連通子圖中的頂點都能被遍歷到   
	for(i=0;i<NUM;i++)
	{	if(!visited[i])
			BFS(Gp,i);		
	}
}

/*
從序號為begin的頂點開始,廣度優先遍歷連通圖Gp
*/
void BFS(Graph Gp,int begin)
{
	//遍歷輸出序號為begin的頂點的資料域,並儲存遍歷資訊
	printf("%c ",Gp[begin].data);
	visited[begin] = true; 

	int i;
	int pVertex;  //用來儲存從佇列中出隊的頂點的序號
	PQUEUE queue = create_queue();  //建立一個空的輔助佇列
	en_queue(queue, begin);          //首先將序號為begin的頂點入隊
	while(!is_empty(queue))
	{
		de_queue(queue,&pVertex);
		//迴圈遍歷,訪問完pVertex頂點的所有鄰接頂點,並將訪問後的鄰接頂點入隊
		for(i=first_vertex(Gp,pVertex); i>=0; i=next_vertex(Gp,pVertex,i))
		{
			if(!visited[i])
			{
				printf("%c ",Gp[i].data);
				visited[i] = true;
				en_queue(queue,i);
			}
		}
	}
	//銷燬佇列,釋放其對應的記憶體
	destroy_queue(queue);
}


遍歷結果

    按照上面的例子來構建圖,而後採用如下程式碼測試DFS和BFS的輸出結果:

/**********************************
圖的BFS和DFS
Author:蘭亭風雨 Date:2014-02-20
Email:zyb_maodun@163.com
**********************************/
#include<stdio.h>
#include<stdlib.h>
#include "data_structure.h"

int main()
{
	Graph Gp = create_graph();

	//深度優先遍歷
	printf("對圖進行深度優先遍歷:\n");
	printf("從頂點A出發DFS的結果:");
	DFS_traverse(Gp,0);
	printf("\n");
	printf("從頂點H出發DFS的結果:");
	DFS_traverse(Gp,7);
	printf("\n");
	printf("從頂點E出發DFS的結果:");
	DFS_traverse(Gp,4);
	printf("\n");
	printf("\n");

	//廣度優先遍歷
	printf("對圖進行深度優先遍歷:\n");
	printf("從頂點A出發BFS的結果:");
	BFS_traverse(Gp,0);
	printf("\n");
	printf("從頂點H出發BFS的結果:");
	BFS_traverse(Gp,7);
	printf("\n");
	printf("從頂點E出發BFS的結果:");
	BFS_traverse(Gp,4);
	printf("\n");

	int i;
	//釋放掉為每個單連結串列所分配的記憶體
	for(i=0;i<NUM;i++)
	{
		free(Gp[i].first);
		Gp[i].first = 0;  //防止懸垂指標的產生
	}

	//釋放掉為頂點陣列所分配的記憶體
	free(Gp);
	Gp = 0;
	return 0;
}

    測試得到的輸出結果如下:

 


完整程式碼下載

    完整的C語言實現程式碼下載地址:http://download.csdn.net/detail/mmc_maodun/6946859




相關文章