數據結構——第六講、圖(圖、圖的遍歷、最短路徑、最小生成樹和拓撲排序)

6.1 什麼是圖

  圖是描述多對多關係的結構,圖包含頂點和邊,頂點用V(Vertex)表示,邊用E(Edge)表示,雙向邊用(v,w)圓括號括住的頂點對錶示,單向邊用<v,w>表示。
操作集:

Graph Create();
Graph InsertVertex(Graph G, Vertex V);
Graph InsertEdge(Graph G, Edge E);
void DFS(Graph G, Vertex V); //從V出發深度優先遍歷圖G
void BFS(Graph G, Vertex V); //從V出發廣度優先遍歷圖G
void ShortestPath(Graph G, Vertex V, int Dist[]);//計算圖G中頂點V到其他任意頂點的最短距離。
void MST(Graph G);//計算圖G的最小生成樹

怎麼在程序中表示圖:

鄰接矩陣:
  對於一個圖有N個節點,節點從0到N-1進行編號,用一個二維數組存儲G[i,j],如果第i個元素和第j個元素之間有邊,那麼就把G[i,j]的值設置爲1,否則設置爲零。
  對於無向圖(即兩個節點之間的邊沒有方向,G[i,j]和G[j,i]等效),我們可以省略一半的存儲空間,用一個(N*(N-1)/2)大小的一維數組來存儲,要找i,j之間的邊可以用(i*(i+1)/2+j)來索引。有向圖不可省略。
  鄰接矩陣查找某個節點鄰接的節點很方便,只需掃描第i行的元素是否爲1即可,對於有權圖,把數組值改爲權值即可,對於有向圖,ij表示從i到j,ji表示從j到i。
  鄰接矩陣存儲稀疏圖(節點很多邊很少)會非常浪費空間,比較適合存稠密圖。
鄰接表:
  稀疏圖用鄰接矩陣會浪費很多空間,那麼我們可以用鄰接表來存儲稀疏圖,鄰接表是一個鏈表類型的數組,有N個節點就有N個元素,每個元素的值都是一個鏈表頭,這個鏈表鏈接着這個元素對應節點的所有的邊,無所謂順序 ,一個接一個把邊存下來,鄰接表存稀疏矩陣比較好,但是終究不如鄰接矩陣方便。

6.2 圖的遍歷

  圖的遍歷有兩種方式:DFS(深度優先搜索)和BFS(廣度優先搜索)。

  • DFS:就是從某一個節點開始,依次訪問它的鄰接點,每個鄰接點都遞歸的調用DFS方法。
void DFS(Vertex V){
	V.Visited = true;
	//對於鄰接表來說,訪問V的每一個鄰接點就是找到V對應的鏈表依次訪問
	//對於鄰接矩陣來說,需要訪問V對應的那一行裏所有的非零(或無窮)項
	for(V的每一個鄰接點W){
		if(W沒有被訪問過)DFS(W);}
}
  • BFS:廣度優先搜索,類似於樹的層序遍歷,用隊列實現。
void BFS(Vertex V){
	V.Visited = true;
	Queue Q;
	AddQ(Q,V);
	while(!IsEmpty(Q)){
		V = Delete(Q);
		for(V的每一個鄰接點W){
			if(W沒有被訪問){
				V.Visited = true;
				AddQ(Q,W);}
	}
}

  兩種遍歷方法各有優劣,不同的情況適用不同的方法。
  有的時候圖並不是聯通的,這時候要遍歷,需要把圖中的每個沒有被訪問的節點都調用一次BFS或DFS,就可以把每個節點都訪問到。(兩種遍歷方法是檢索數據的方式,並不是說找不到這些數據了,而是按照某種特定方法來遍歷,達到某些目的)。

7.1 最短路徑問題

  最短路徑問題分爲:單源最短路徑和多源最短路徑。
單源最短路徑:
  單源無權圖的算法思想:從源點開始一圈一圈往外擴展,依次找到與源點距離爲1的,與源點距離爲2的節點,對廣度優先搜索(BFS)稍作修改即可。

void Unweighted( Vertex S ){
	int Dest[N] = {-1};
	Vertex Path[];
	EnQueue( S,Q );
	Dest[S] = 0;
	while( !IsEmpty Q ){
		V = DelQueue(Q);
		for(V的每一個鄰接點W){
			if( Dist[W] == -1){
				//到W的距離等於到V的距離加1
				Dist[W] = Dist[V]+1;
				//到達W的最短路徑必須經過V
				Path[W] = V;
			}
		}
	}
}
  • 單源有權圖的算法(dijkstra算法):有一個集合S,它裏面收錄了源點和已經找到最短路徑的點,按照距離非遞減的順序依次把所有的點都收錄到S裏面。
//需要把Dist初始化爲正無窮,Path初始化爲-1
void Dijkstra(VerTex V){
	Dest[V] = 0;
	Collected[V] = true;
	for(V的每一個鄰接點W)//E<V,W>bi表示V到W的距離,也就是權重。
		Dest[W] = E<V,W>;while(1){
		V = 還未收錄的節點的Dest最小的節點。
		if(所有的節點都被收錄)break;
		Collected[V] = true;
		for(V的每一個鄰接點){
			if( Collected[W] == false ){
				if(Dist[V] + E<V,W> < Dist[W]){
					Dist[W] = Dist[V] + E<V,W>;
					Path[W] = V;
				}
			}
		}
	}
}

多源最短路徑:
方法一:可以直接把單源最短路徑的方法對每一個節點都調用一遍(對稀疏圖效果好)。
方法二:Floyd算法

bool Floyd( MGraph Graph, WeightType D[][MaxVertexNum], Vertex path[][MaxVertexNum] )
{
    Vertex i, j, k;
    /* 初始化 */
    for ( i=0; i<Graph->Nv; i++ )
        for( j=0; j<Graph->Nv; j++ ) {
            D[i][j] = Graph->G[i][j];
            path[i][j] = -1;
        }
    for( k=0; k<Graph->Nv; k++ )
        for( i=0; i<Graph->Nv; i++ )
            for( j=0; j<Graph->Nv; j++ )
                if( D[i][k] + D[k][j] < D[i][j] ) {
                    D[i][j] = D[i][k] + D[k][j];
                    if ( i==j && D[i][j]<0 ) /* 若發現負值圈 */
                        return false; /* 不能正確解決,返回錯誤標記 */
                    path[i][j] = k;
                }
    return true; /* 算法執行完畢,返回正確標記 */
}

8.1 最小生成樹問題

最小生成樹: 一個圖構成最小生成樹,圖裏面的每一個聯通的節點都必須包含在生成樹裏面,生成樹的邊最少即(N-1)條,不構成迴路,而且選出的邊的權重和必須最小。
有兩種算法:Prim算法和KrusKal算法

  • Prim算法:從一個根節點開始,依次選出可以選的邊,選擇的邊要是權重最小的,且不會構成迴路。適用於邊比節點多很多。
void prim(VerTex V){
	//根節點的parent就是-1,非根節點的parent是它的父元素。
	parent[V] = -1;
	//根節點的dest爲0,根節點的鄰接點dest爲邊的距離,其餘的dest都是無窮大。
	dest[V] = 0;
	for(V的每一個鄰接點W){
		dest[W] = E<V,W>;
	}
	while(1){
		V = 全部節點中dest最小的非零節點;
		if(沒有這樣的節點V)
			break;
		//將V收錄進MST
		dest[V] = 0;
		for(V的每一個鄰接點W){
			//如果W沒有被收錄,即W的dest不爲零
			if(dest[W] != 0 && E<W,V> < dest[W]){
				dest[W] = E<W,V>;
				parent[W] = V;
			}
		}
	}
	if(MST中的節點不足|V|)
		Error("生成樹不存在!")
}
  • KrusKal算法:每次選擇權重最小的,不會構成迴路的邊。適用於邊和節點屬於同一數量級的
void kruskal(VerTex V){
	//MST初始爲空。
	MST = {};
	while(MST中的邊不到N-1&& 邊集E中還有邊存在){
		//最小堆
		//個人思路:構造一個存儲邊的結構體,包含這條邊兩端的節點信息
		從E中取出一條權重最小的邊E<V,W>;
		將E<V,W>從邊集中刪除;
		//並查集檢查選中邊的兩個節點是否在同一個集合中
		//個人思路:將邊加入MST中時,同時將邊的兩個節點標記爲已經加入,判斷是否構成迴路就判斷這條邊兩個節點是否都已經加入。
		if(E<V,W>添加到MST中不會構成迴路)
			將E<V,W>加入MST中
	}
	if(MST中的邊不到N-1)
		Error("生成樹不存在");
}

8.1 拓撲排序

  拓撲序: 如果圖中從V到W有一條有向路徑,則V一定排在W之前。滿足此條件的頂點序列稱爲一個拓撲序。獲得一個拓撲序的過程就是拓撲排序。AOV如果有合理的拓撲序,則必定是一個有向無環圖(DAG)。

void topSort(){
	for(圖中的每一個頂點V)
		if(Indegree[V] == 0)
			InQueue(Q,V)
	while(!IsEmpty(Q)){
		V = Dequeue(Q);
		輸出V,或者記錄V的輸出序號。
		for(V的每一個鄰接點W)
			if(--Indegree[W] == 0)
				InQueue(Q,W)
	}
	if(輸出的個數不足|V|)
		Error("圖中有迴路");
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章