圖的遍曆
圖的遍曆(Traversing Graph):從圖的某一頂點出發,訪遍圖中的其餘頂點,且每個頂點僅被訪問一次。
圖的遍曆演算法是各種圖的操作的基礎。但圖的遍曆存在以下特點:
◆ 複雜性:圖的任意頂點可能和其餘的頂點相鄰接,可能在訪問了某個頂點後,沿某條路徑搜尋後又回到原頂點,而有些頂點卻還沒有被遍曆到的情況。
◆ 解決辦法:在遍曆過程中記下已被訪問過的頂點。設定一個輔助向量Visited1…n,其初值為0,一旦訪問了頂點vi後,使Visited[i]為1或為訪問的序號。
圖的遍曆演算法有深度優先搜尋演算法和廣度優先搜尋演算法。 深度優先搜尋(Depth First Search–DFS)
深度優先搜尋(Depth First Search–DFS)遍曆類似樹的先序遍曆,是樹的先序遍曆的推廣。
1 演算法思想
設初始狀態時圖中的所有頂點未被訪問,則:
⑴ :從圖中某個頂點vi出發,訪問vi;然後找到vi的一個鄰接頂點vi1 ;
⑵:從vi1出發,深度優先搜尋訪問和vi1相鄰接且未被訪問的所有頂點;
⑶:轉⑴ ,直到和vi相鄰接的所有頂點都被訪問為止
⑷:繼續選取圖中未被訪問頂點vj作為起始頂點,轉(1),直到圖中所有頂點都被訪問為止。
演算法實現
由演算法思想知,這是一個遞迴過程。因此,先設計一個從某個頂點(編號)為v0開始深度優先搜尋的函數,便於調用。
在遍曆整個圖時,可以對圖中的每一個未訪問的頂點執行所定義的函數。
typedef emnu {FALSE , TRUE} BOOLEAN ;BOOLEAN Visited[MAX_VEX] ;
3 結點及其類型定義#define MAX_VEX 30 /* 最大頂點數 */typedef char VertexType; /* 頂點類型應由使用者定義 */typedef int EdgeType; /* 邊上的權實值型別應由使用者定義 */typedef struct EdgeNode /* 邊表結點 */{ int adjvex; /* 鄰接點域,儲存該頂點對應的下標 */ EdgeType info; /* 用於儲存權值,對於非網圖可以不需要 */ struct EdgeNode *next; /* 鏈域,指向下一個鄰接點 */}EdgeNode;
typedef struct VertexNode /* 頂點表結點 */{ VertexType data; /* 頂點域,儲存頂點資訊 */ /* int indegree ; //頂點的度, 有向圖是入度或出度 (亦可不要) */ EdgeNode *firstarc; /* 邊表頭指標 */}VertexNode, AdjList[MAX_VEX];typedef struct{ AdjList adjList; //頂點數組(亦成為頭結點向量) int numNodes,numEdges; /* 圖中當前頂點數和邊數 */}GraphAdjList;
void DFS( GraphAdjList *GL , int v){ EdgeNode *p ;Visited[v]=TRUE ; Visit[v] ; /* 置訪問標誌,訪問頂點v */ p=GL->adjList[v].firstarc; /* 鏈表的第一個結點 */while (p!=NULL){ if (!Visited[p->adjvex]) DFS(GL, p->adjvex) ; // 從v的未訪問過的鄰接頂點遞迴調用 p=p->next ;} }void DFSTraverse (GraphAdjList *GL){ int v ; for (v=0 ; v<GL->vexnum ; v++) Visited[v]=FALSE ; /* 訪問標誌初始化 */ for (v=0 ; v<GL->vexnum ; v++) if (!Visited[v]) DFS(GL , v);}
3 演算法分析
採用鄰接表作為儲存結構遍曆時,找鄰接點所需的時間取決於頂點和邊的數量,總時間複雜度為O(n+e) 。
採用鄰接矩陣的深度優先遞迴演算法void DFS(MGraph G, int i){ int j; visited[i] = TRUE; Visit[v] ; for(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,若是連通圖,只會執行一次 DFS(G, i);}
廣度優先搜尋(Breadth First Search–BFS)
廣度優先搜尋(Breadth First Search–BFS)遍曆類似樹的按層次遍曆的過程。
1 演算法思想
設初始狀態時圖中的所有頂點未被訪問,則:
⑴ :從圖中某個頂點vi出發,訪問vi;
⑵:訪問所有與vi相鄰接且未被訪問的所有頂點vi1,vi2,…,vim;
⑶:以vi1,vi2, …,vim的次序,以vij(1≦j≦m)依次作為vi ,轉⑴;
⑷ :繼續選取圖中未被訪問頂點vk作為起始頂點,轉⑴,直到圖中所有頂點都被訪問為止。
2 演算法實現
為了標記圖中頂點是否被訪問過,同樣需要一個訪問標記數組;
在訪問vi時,為了依次訪問與vi相鄰接的各個頂點,需要附加一個隊列來儲存與vi相鄰接的頂點。
bool Visited[MAX_VEX] ;typedef struct Queue{ int elem[MAX_VEX] ;int front , rear ;}Queue ; /* 定義一個隊列儲存將要訪問頂點 */
void BFSTraverse (GraphAdjList *G){ 建立空隊列並初始化 (Q->front=Q->rear=0) 訪問標誌初始化 (Visited[i]=false,0≤i< G->vexnum ) for (i=0 ; i<G->vexnum ; i++) { if (!visited[i]) { visited[i]=true; 訪問該頂點;然後將讓該頂點入隊, while(!QueueEmpty(Q)) { DeQueue(&Q,&i); p = GL->adjList[i].firstedge; //找到當前頂點的邊鏈表頭指標 while(p) { if(!visited[p->adjvex]) /* 若此頂點未被訪問 */ { visited[p->adjvex]=true; 訪問該頂點;然後將讓該頂點入隊 } //end of if p = p->next; /* 指標指向下一個鄰接點 */ }//end of while } } }}
廣度優先搜尋演算法和深度優先搜尋演算法在時間複雜度上是一樣的,不同之處僅僅在於頂點訪問次序不同。
圖的遍曆可以系統地訪問圖中的每個頂點,因此,圖的遍曆演算法是圖的最基本、最重要的演算法,許多有關圖的操作都是在圖的遍曆基礎之上加以變化來實現的。 最小產生樹
如果連通圖是一個帶權圖,則其產生樹中的邊也帶權,產生樹中所有邊的權值之和稱為產生樹的代價。
最小產生樹(Minimum Spanning Tree) :帶權連通圖中代價最小的產生樹稱為最小產生樹。
構造最小產生樹的演算法有許多,最經典的有兩種:
普裡姆(Prim)演算法
克魯斯卡爾(Kruskal)演算法 普裡姆(Prim)演算法
從連通網N=(U,E)中找最小產生樹T=(U,TE) 。1 演算法思想⑴ 若從頂點v0出發構造,U={v0},TE={};⑵ 先找權值最小的邊(u,v),其中u∈U且v∈V-U,並且子圖不構成環,則U= U∪{v},TE=TE∪{(u,v)} ;⑶ 重複⑵ ,直到U=V為止。則TE中必有n-1條邊, T=(U,TE)就是最小產生樹。
2 演算法實現說明
設用鄰接矩陣(二維數組)表示圖,兩個頂點之間不存在邊的權值為機器允許的最大值。
為便於演算法實現,設定一個一維數組closedge[n],用來儲存V- U中各頂點到U中頂點具有權值最小的邊。數組元素的類型定義是
struct { int adjvex ; /* 權值最小的邊所依附於U中的頂點 */int lowcost ; /* 該邊的權值 */}closedge[MAX_EDGE] ;如:closedge[j].adjvex=k,表明邊(vj, vk)是V-U中頂點vj到U中權值最小的邊,而頂點vk是該邊所依附的U中的頂點。 closedge[j].lowcost存放該邊的權值。
在Prime演算法中,圖採用鄰接矩陣儲存,所構造的最小產生樹用一維數組儲存其n-1條邊,每條邊的儲存結構描述:typedef struct MSTEdge{ int vex1, vex2 ; /* 邊所依附的圖中兩個頂點 */WeightType weight ; /* 邊的權值 */}MSTEdge ;演算法實現(略講)#define INFINITY MAX_VAL /* 最大值 */ MSTEdge *Prim_MST(AdjGraph *G , int u) /* 從第u個頂點開始構造圖G的最小產生樹 */{ MSTEdge TE[] ; // 存放最小產生樹n-1條邊的數組指標 int j , k , v , min ; for (j=0; j<G->vexnum; j++) { closedge[j].adjvex=u ; closedge[j].lowcost=G->adj[j][u] ; } /* 初始化數組closedge[n] */ closedge[u].lowcost=0 ; /* 初始時置U={u} */ TE=(MSTEdge *)malloc((G->vexnum-1)*sizeof(MSTEdge)) ;for (j=0; j<G->vexnum-1; j++){ min= INFINITY ; for (v=0; v<G->vexnum; v++) if (closedge[v].lowcost!=0&& closedge[v].Lowcost<min) { min=closedge[v].lowcost ; k=v ; } TE[j].vex1=closedge[k].adjvex ; TE[j].vex2=k ; TE[j].weight=closedge[k].lowcost ; closedge[k].lowcost=0 ; /* 將頂點k併入U中 */ for (v=0; v<G->vexnum; v++) if (G->adj[v][k]<closedge[v]. lowcost) { closedge[v].lowcost= G->adj[v][k] ; closedge[v].adjvex=k ; } /* 修改數組closedge[n]的各個元素的值 */ } return(TE) ;} /* 求最小產生樹的Prime演算法 */
克魯斯卡爾(Kruskal)演算法
1 演算法思想
演算法思想:設連通網N=(V,E),令最小產生樹
初始狀態為只有n個頂點而無邊的非連通圖T=(V,),每個頂點自成一個連通分量
在E中選取代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中;否則,捨去此邊,選取下一條代價最小的邊
依此類推,直至T中所有頂點都在同一連通分量上為止
2 演算法實現說明
Kruskal演算法實現的關鍵是:當一條邊加入到TE的集合後,如何判斷是否構成迴路?
解決方案:定義一個一維數組Vset[n] ,存放圖T中每個頂點所在的連通分量的編號。
Vset[n] :
◆ 初值:Vset[i]=i,表示每個頂點各自組成一個連通分量,連通分量的編號簡單地使用頂點在圖中的位置(編號)。
◆ 當往T中增加一條邊(vi,vj) 時,先檢查Vset[i]和Vset[j]值:
☆ 若Vset[i]=Vset[j]:表明vi和vj處在同一個連通分量中,加入此邊會形成迴路;
☆ 若Vset[i]≠Vset[j],則加入此邊不會形成迴路,將此邊加入到產生樹的邊集中。
◆ 加入一條新邊後,將兩個不同的連通分量合并:將一個連通分量的編號換成另一個連通分量的編號。
演算法分析:設帶權連通圖有n個頂點,e條邊,則演算法的主要執行是:
◆ Vset數組初始化:時間複雜度是O(n) ;
◆ 邊表按權值排序:若採用堆排序或快速排序,時間複雜度是O(e㏒e) ;
◆ while迴圈:最大執行頻度是O(n),其中包含修改Vset數組,共執行n-1次,時間複雜度是O(n2) ;
整個演算法的時間複雜度是O(e㏒e+n2) 。