一:圖的抽象數據類型
ADT 圖(Graph) Data 頂點的有窮非空集合和邊的集合 Operation CreateGraph(*G,V,VR):按照頂點集V和邊弧集VR的定義構造圖G DestroyGraph(*G):圖G存在則銷毀 LocateVex(G,u):若圖G中存在頂點u,則返回圖中位置 GetVex(G,v):返回圖中頂點v的值 PutVex(G,v,value):將圖G中頂點v賦值給value FirstAdjVex(G,*v):返回頂點v的一個鄰接頂點,若頂點在G中無鄰接頂點則返回空 NextAdjVex(G,v,*w):返回頂點v相對於頂點w的下一個鄰接頂點,若w是v的最后一個鄰接點則返回空 InsertVex(*G,v):在圖G中增加新頂點v DeleteVex(*G,v):刪除圖G中頂點v及其相關的弧 InsertArc(*G,v,w):在圖G中添加弧<v,w>,若G是無向圖,還需要添加對稱弧<w,v> DeleteArc(*G,v,w):在圖G中刪除弧<v,w>,若G是無向圖,則需要刪除對稱弧<w,v> DFSTraverse(G):對圖G中進行深度優先遍歷,在遍歷過程對每個頂點調用 HFSTraverse(G):對圖G中進行廣度優先遍歷,在遍歷過程對每個頂點調用 endADT
二:圖的存儲結構討論
對於線性表來說,是一對一的關系,所以用數組或者鏈表均可以簡單存放。
對於樹結構是一對多的關系,所以我們要將數組和鏈表的特性結合在一起才能更好的存放。
對於圖來說,是多對多的情況,另外圖上的任意一個頂點都可以被看做是第一個頂點,任一頂點的鄰接點之間也不存在次序關系
如下圖:實際是一個圖結構,只不過頂點位置不同。

由於圖的結構復雜,任意兩個頂點之間都可能存在聯系,因此無法以數據元素在內存中的物理位置來表示元素之間的關系,也就是說,圖不可能用簡單的順序存儲結構來表示。
內存物理位置是線性的,圖的元素關系是平面的。
雖然我們可以向樹結構中說到的那樣使用多重鏈表,但是我們需要先確定最大的度,然后按照這個度最大的頂點設計結點結構,若是每個頂點的度數相差較大,就會造成大量的存儲單元浪費。
三:圖的存儲結構(1)---鄰接矩陣
考慮到圖是由頂點和邊(弧)兩部分組成的,合在一起是比較困難的,那就很自然的考慮到分為兩個結構來分別存儲
頂點因為不區分大小,主次,所以用一個一維數組來存儲時不錯的選擇。 而邊或弧由於是頂點與頂點之間的關系,所以我們最好使用二維數組來存儲
圖的鄰接矩陣存儲方式是用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱為鄰接矩陣)存儲圖中的邊或弧的信息。

(一)無向圖

其中1表示兩個頂點之間存在關系,0表示無關,不存在頂點間的邊。
對稱矩陣:就是n階矩陣滿足a[i][j]=a[j][i](0<=i,j<=n)。即從矩陣的左上角到右下角的主對角線為軸,右上角的源與左下角相對應的元都是相等的。
根據這個矩陣,我們可以很容易的知道圖中的信息。 1.我們要判定容易兩頂點是否有邊無邊就非常容易了。 2.我們要知道某個頂點的度,其實就是這個頂點vi在鄰接矩陣中第i行(或第i列)的元素之和。比如上圖頂點v1的度就是1+0+1+0=2 3.求頂點vi的所有鄰接點就是將矩陣第i行元素掃描一遍,arc[i][j]為1就是鄰接點
(二)有向圖
對於上面的無向圖,二維對稱矩陣似乎浪費了一半的空間。若是存放有向圖,會更大程度利用起來空間

其中頂點數組是一樣的和無向圖,弧數組也是一個矩陣,但因為是有向圖,所以這個矩陣並不對稱:例如v1->v0有弧,arc[1][0]=1,而v0到v1沒有弧,所以arc[0][1]=0。 另外有向圖,要考慮入度和出度,頂點v1的入度為1,正好是第v1列的各數之和,頂點v1的出度為2,正好是第v2行的各數之和
(三)網
每條邊上帶有權的圖就叫做網

這里‘∞’表示一個計算機允許的,大於所有邊上權值的值
(四)實現無向網圖創建
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXVEX 100 //最大頂點數 #define INFINITY 65535 //用65535表示∞ typedef char VertexType; //頂點類型,字符型A,B,C,D... typedef int EdgeType; //邊上權值類型10,15,... typedef struct { VertexType vers[MAXVEX]; //頂點表 EdgeType arc[MAXVEX][MAXVEX]; //鄰接矩陣,可看作邊表 int numVertexes, numEdges; //圖中當前的頂點數和邊數 }MGraph; void CreateMGraph(MGraph* G) { int i, j, k, w; printf("please input number of vertex and edge:\n"); scanf("%d,%d", &G->numVertexes, &G->numEdges); //輸入頂點數和邊數 getchar(); //可以獲取回車符 for (i = 0; i < G->numVertexes; i++) //讀入頂點信息,建立頂點表 scanf("%c", &G->vers[i]); getchar(); //可以獲取回車符 for (i = 0; i < G->numVertexes;i++) for (j = 0; j < G->numVertexes;j++) G->arc[i][j] = INFINITY; //鄰接矩陣初始化 for (k = 0; k < G->numEdges;k++) //讀入numEdges條邊,建立鄰接矩陣 { printf("input edge(vi,vj) row(i),col(j),weight(w):\n"); scanf("%d,%d,%d", &i, &j, &w); //輸入邊(vi,vj),以及上面的權值 getchar(); //可以獲取回車符 G->arc[i][j] = w; G->arc[j][i] = G->arc[i][j]; //因為是無向圖,所有是對稱矩陣 } } int main() { MGraph MG; CreateMGraph(&MG); system("pause"); return 0; }
n個頂點,e條邊創建無向網圖,時間復雜度是O(n+n^2+e),初始化時耗費了O(n^2)



四:圖的存儲結構(2)---鄰接表
上面的鄰接矩陣是一種不錯的圖存儲結構,便於理解,但是當我們的邊數相對於頂點較少的圖,這種結構是存在對存儲空間的極大的浪費。

我們可以考慮使用鏈表來動態分配空間,避免使用數組一次分配而造成空間浪費問題。
同樹中,孩子表示法,我們將結點存放入數組,對結點的孩子進行鏈式存儲,不管有多少孩子,都不會存在空間浪費。這種思路也適用於圖的存儲。我們把這種數組與鏈表相結合的存儲方法稱為鄰接表
鄰接表處理辦法
1.圖中頂點用一個一維數組。當然,頂點也可以用單鏈表來存儲,不過數組可以較容易的讀取頂點信息,更加方便。另外,對於頂點數組中,每個數據元素還需要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息
2.圖中每個頂點vi的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表存儲,無向圖稱為頂點vi的邊表,有向圖則稱為頂點vi作為弧尾的出邊表
(一)無向圖

這樣的結構,對於我們要獲得圖的相關信息也是很方便。比如:
我們要獲取某個頂點的度,就要去查找這個頂點的邊表中結點的個數。 我們要判斷vi到vj是否存在邊,只需要測試vi的邊表鏈表中是否存在結點vj的下標j即可。 我們若是要獲取頂點的所有鄰接點,就是對此頂點的邊表進行遍歷。
(二)有向圖
有向圖由於有方向,我們是以頂點為弧尾來存儲邊表的,這樣很容易就可以得到每個頂點的出度。但是由於有時也需要確定頂點的入度或以頂點作為弧頭的弧,我們可以建立一個有向圖的逆鄰接表,即對每個頂點vi都建立一個鏈接為vi為弧頭的表


(三)帶權值的網圖
我們可以在邊表結點定義中再增加一個weight數據域,存儲權值信息即可

(四)實現無向網圖
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXVEX 100 //最大頂點數 typedef char VertexType; //頂點類型,字符型A,B,C,D... typedef int EdgeType; //邊上權值類型10,15,... typedef struct EdgeNode //邊表結點 { int adjvex; //鄰接點域,存放該頂點對應的下標 int weight; //用於存放權值,對於非網圖可以不需要 struct EdgeNode* next; //鏈域,指向下一個鄰接點 }EdgeNode; typedef struct VertexNode //頂點表結點 { VertexType data; //頂點域,存儲頂點信息 EdgeNode* firstedge; //邊表頭指針 }VertexNode,AdjList[MAXVEX]; typedef struct { AdjList adjList; //鄰接表數組 int numVertexes, numEdges; //圖中所存儲的頂點數和邊數 }GraphAdjList; void CreateALGraph(GraphAdjList* G) { int i, j ,k,w; EdgeNode *e; printf("please input number of vertex and edge:\n"); scanf("%d,%d",&G->numVertexes,&G->numEdges); //輸入頂點數和邊數 getchar(); //可以獲取回車符 for (i = 0; i < G->numVertexes;i++) //輸入頂點信息 { scanf("%c", &G->adjList[i].data); //輸入頂點信息 G->adjList[i].firstedge = NULL; //將邊表置為空 } getchar(); //可以獲取回車符 for (k = 0; k < G->numEdges;k++) { printf("input edge(vi,vj) vertexs series and the weight:\n"); scanf("%d,%d,%d", &i, &j,&w); getchar(); //由於是無向圖,對稱矩陣,當我們設置邊以后,需要在兩個地方設置結點 e = (EdgeNode *)malloc(sizeof(EdgeNode)); //使用頭插法將數據插入(主要是頭插法方便),我們插入不需要考慮順序,因為鏈表結點都是與數組頂點相連接的 e->adjvex = j; e->next = G->adjList[i].firstedge; e->weight = w; G->adjList[i].firstedge = e; e = (EdgeNode*)malloc(sizeof(EdgeNode)); e->adjvex = i; e->next = G->adjList[j].firstedge; e->weight = w; G->adjList[j].firstedge = e; } } int main() { GraphAdjList gl; CreateALGraph(&gl); system("pause"); return 0; }




注意:上面的兩種存儲結構是針對頂點,下面的三種存儲結構是針對邊
五:圖的存儲結構(3)---十字鏈表
我們想要知道出度方向的頂點,可以使用鄰接表,我們要了解入度就需要使用逆鄰接表。但是我們既想要了解入度有想要了解出度,那么我們該如何處理?
這時就需要使用到有向圖的一種存儲方法:十字鏈表
頂點表結點結構

firstin表示入邊表頭指針,指向該頂點的入邊表中第一個結點。
firstout表示出邊表頭指針,指向該頂點的出邊表中第一個結點。
邊表結點結構

其中tailvex是指弧起點在頂點表的下標,headvex是指弧終點在頂點表的下標。
headlink是指入邊表指針域,指向終點相同的下一條邊,taillink是指邊表指針域,指向起點相同的下一條邊。
如果是網,我們還要在其中加入權值域,來存儲權值

我們可以看做橫向是出度,豎向是入度
頂點的出度和入度。除了結構復雜一點外,其實創建圖算法的時間復雜度和鄰接表是相同的,因此很好的應用在有向圖中。
注意:整張圖的出度和入度是一致的(不是某個頂點,而是這張圖)
代碼實現
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXVEX 100 //最大頂點數 typedef char VertexType; //頂點類型,字符型A,B,C,D... typedef int EdgeType; //邊上權值類型10,15,... typedef struct OLNode //十字鏈表結點 { int tailvex, headvex; //表示兩個頂點下標,構成了一條邊。tailvex是箭頭尾,弧尾,headvex是箭頭頭,是弧頭 int weight; //用於存放權值,對於非網圖可以不需要 struct OLNode* taillink; //十字鏈域,指向下一個鄰接點,是出度方向 struct OLNode* headlink; //十字鏈域,指向下一個鄰接點,是入度方向 }OLNode; typedef struct VertexNode //頂點表結點 { VertexType data; //頂點域,存儲頂點信息 OLNode* firstin; //邊表頭指針,入度 OLNode* firstout; //邊表頭指針,出度 }VertexNode, CrossList[MAXVEX]; typedef struct { CrossList adjList; //鄰接表數組 int numVertexes, numEdges; //圖中所存儲的頂點數和邊數(出度指針和入度指針是和邊數是一致的) }GraphCrossList; void CreateCrossGraph(GraphCrossList* G) { int i, j ,k,w; OLNode *e; printf("please input number of vertex and edge:\n"); scanf("%d,%d",&G->numVertexes,&G->numEdges); //輸入頂點數和邊數 getchar(); //可以獲取回車符 for (i = 0; i < G->numVertexes;i++) //輸入頂點信息 { scanf("%c", &G->adjList[i].data); //輸入頂點信息 G->adjList[i].firstin = NULL; //將邊表置為空 G->adjList[i].firstout = NULL; //將邊表置為空 } getchar(); //可以獲取回車符 //先循環獲取出度結點,出度數和入度數是一致的 for (k = 0; k < G->numEdges;k++) { printf("Out-->input edge(vi,vj) vertexs series and the weight:\n"); scanf("%d,%d,%d", &i, &j,&w); getchar(); //創建十字鏈表結點 e = (OLNode *)malloc(sizeof(OLNode)); //使用頭插法將數據插入(主要是頭插法方便),我們插入不需要考慮順序,因為鏈表結點都是與數組頂點相連接的 e->tailvex = i; e->headvex = j; e->weight = w; e->taillink = G->adjList[i].firstout; G->adjList[i].firstout = e; } //再循環獲取入度結點,在上面的出度結點建立了所有結點的基礎上,我們再建立入度指針指向 //例如我們要獲取v0的入度v1,我們先輸入入度點0,再輸入出度點1,不需要權值,權值在上面對有向圖都賦值了 //我們進行第二個指針v0的入度v2,我們使用相同輸入,但是開始使用頭插法修改鏈表firstin指向 for (k = 0; k < G->numEdges; k++) { printf("In-->input edge(vi,vj) vertexs series:\n"); scanf("%d,%d", &i, &j); getchar(); //根據上面的輸入,我們將當前的頂點域firstin指向我們獲取的新的入度域 //先找入度域 e = G->adjList[j].firstout; while (e->headvex!=i) e = e->taillink; //循環十字鏈表結點,獲取我們要的入度結點 //使用頭插法插入入度指針域 e->headlink = G->adjList[i].firstin; G->adjList[i].firstin = e; } } int main() { GraphCrossList gl; CreateCrossGraph(&gl); gl; system("pause"); return 0; }



六:圖的存儲結構(4)---鄰接多重表
鄰接多重表是對無向圖的存儲結構的優化
問題:
對於無向圖的鄰接表,我們更加關注的重點是頂點,那么是不錯的選擇,但是我們要是關注的是邊的操作。
比如:刪除一條邊,那么我們的操作將變得復雜,我們需要找到這條邊的兩個頂點,方便去其鏈表中刪除所表示的邊。稍微有點麻煩。

改進:
我們只出現無向圖中對應條數的邊表結點,其他的結構,我們全部由指針來聯系,所以當我們想要刪除一條邊時,就只需要刪除對應的邊表結點。指向她的指針會置為空,他自己產生的指針會消失。就完成了對邊的操作。

例如上圖,我們若是使用鄰接表:是要10個頂點結點去表示5條邊,而我們若是使用鄰接多重表,只需要5個邊結點即可。刪除一條邊就不存在重復操作
定義
鄰接多重表結構

其中ivex和jvex是指某條邊依附的兩個頂點在頂點表中的下標。ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。
如上圖有4個頂點和5條邊,先將邊表結點畫出來。由於是無向圖,所以ivex,jvex正反過來都可以,為了繪圖方便,都將ivex值設置的與一旁的頂點下標相同
1.將邊表結點畫出來

2.開始連線
首先連線的(1)(2)(3)(4)是將頂點的firstedge指向一條邊,頂點下標要與ivex的值相同。

接着,由於頂點v0的(v0,v1)邊的鄰邊有(v0,v3)和(v0,v2)。因此(5)(6)的連線就是滿足指向下一條依附於頂點v0的邊的目標,注意ilink指向的結點的jvex(ivex)一定要與它本身的ivex的值相同。
注意ilink指向的結點的jvex(ivex)一定要與它本身的ivex的值相同。而且為了方便,我們和jvex相同,那么全部指向都要與之一樣

同理,連線(7)就是指(v1,v0)這條邊,它是相當於頂點v1指向(v1,v2)邊后的下一條。

v2有三條邊依附,所以(3)之后就有了(8)(9)。

連線(10)就是頂點v3在連線(4)之后的下一條邊

左圖一共有5條邊,所以右圖有10條連線,完全符合預期。
總結
鄰接多重表與鄰接表的差別,僅僅是在於同一條邊在鄰接表中用兩個邊表結點表示,而在鄰接多重表中只有一個結點。這樣對邊的操作就方便多了,
若要刪除左圖的(v0,v2)這條邊,只需要將右圖的(6)(9)的鏈接指向改為^即可。
代碼實現前的思考:
MMP,怎么就想不出來用什么方法去存儲這些邊呢?頂點和邊不對應呀....吐血....,去網上找找其他關於鄰接多重表的信息吧
數據結構之圖(2-2)【鄰接多重表】適用於無向圖
無向圖的鄰接多重表存儲結構
這兩篇文章來了思路,將表的結構修改一下,我們只需要保證鄰接多重表的根本,就是創建邊表,而不是創建頂點表,因為頂點表是邊表的兩倍,在刪除時會導致重復操作,而我們的邊表只需要創建對應邊數的結點,刪除某條邊,就刪除對應結點即可。不需要重復操作。


上面這種表示方法,但是好像沒說清楚為啥這么排列。
於是決定測試將對應頂點的鄰接邊全部放入頂點后面
步驟一:先排序第一個頂點的所有鄰接邊

步驟二:我們再排序第二個頂點的所有鄰接邊

步驟三:我們接着排序第三個頂點的所有鄰接邊

步驟四:開始排序最后一個頂點所有鄰接邊

通過上面排序,我們可以獲取所有鄰接點和鄰接邊
按照上面來實現代碼

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXVEX 100 //最大頂點數 typedef char VertexType; //頂點類型,字符型A,B,C,D... typedef int EdgeType; //邊上權值類型10,15,... typedef struct ENode //十字鏈表結點 { int ivex, jvex; //表示兩個頂點下標,構成了一條邊。 int weight; //用於存放權值,對於非網圖可以不需要 struct ENode* ilink; //鄰接多重表,指向下一個鄰接點, struct ENode* jlink; //鄰接多重表,指向下一個鄰接點, }ENode; typedef struct VertexNode //頂點表結點 { VertexType data; //頂點域,存儲頂點信息 ENode* firstedge; //邊表頭指針 }VertexNode, AMLList[MAXVEX]; typedef struct { AMLList adjList; //鄰接多重表數組 int numVertexes, numEdges; //圖中所存儲的頂點數和邊數 }AMLGraphList; //根據ivex找到第幾行,我們去前面幾行查找,jvex獲取下標 ENode* GetNode(AMLGraphList* G,int ivex,int jvex) { int i; ENode *e; for (i = ivex-1; i >=0;i--) { e = G->adjList[i].firstedge; while (e) { if (e->jvex==jvex) { return e; } e = e->ilink; } } return NULL; } void CreateAMLGraph(AMLGraphList* G) { int i, j, k, w,flag; ENode *e; ENode *tempNode; printf("please input number of vertex and edge:\n"); scanf("%d,%d", &G->numVertexes, &G->numEdges); //輸入頂點數和邊數 getchar(); //可以獲取回車符 for (i = 0; i < G->numVertexes; i++) //輸入頂點信息 { scanf("%c", &G->adjList[i].data); //輸入頂點信息 G->adjList[i].firstedge = NULL; //將邊表置為空 } getchar(); //可以獲取回車符 //先循環獲取邊信息。創建所有邊信息,放在對應的頂點后面, for (k = 0; k < G->numEdges; k++) { printf("Out-->input edge(vi,vj) vertexs series and the weight:\n"); scanf("%d,%d,%d", &i, &j, &w); getchar(); //創建鄰接多重表結點 e = (ENode *)malloc(sizeof(ENode)); //使用頭插法將數據插入(主要是頭插法方便),我們插入不需要考慮順序,因為鏈表結點都是與數組頂點相連接的 e->ivex = i; e->jvex = j; e->weight = w; e->ilink = G->adjList[i].firstedge; e->jlink = NULL; G->adjList[i].firstedge = e; } //開始連接多張表之間的關系,並且判斷ilink和jlink,從第二個頂點開始 for (i = 1; i < G->numVertexes;i++) { e = G->adjList[i].firstedge; if (e) { flag = 0; //用來標識是不是最后一個結點 while (e&&!flag) //需要將最后一個單獨處理 { if (e->ilink == NULL) { tempNode = GetNode(G, i, e->ivex); e->ilink = tempNode; flag = 1; } tempNode = GetNode(G, i, e->jvex); e->jlink = tempNode; e = e->ilink; } //e = GetNode(G, i, e->jvex); //處理最后一個 } else G->adjList[i].firstedge = GetNode(G, i, i); } } int main() { AMLGraphList gl; CreateAMLGraph(&gl); gl; system("pause"); return 0; }





七:圖的存儲結構(5)---邊集數組
邊集數組是由兩個一維數組構成。一個是存儲頂點的信息;另一個是存儲邊的信息。
這個邊數組每個數據元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成。
結構

表現

如上圖所示,邊集數組關注的是邊的集合,在邊集數組中要查找一個頂點的度需要掃描整個邊數組,效率並不高。
因此它更適合對邊依次進行處理的操作,而不適合對頂點相關的操作。
代碼實現
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXVEX 100 //最大頂點數 typedef char VertexType; //頂點類型,字符型A,B,C,D... typedef int EdgeType; //邊上權值類型10,15,... typedef struct VertexNode //頂點表結點 { VertexType data; //頂點域,存儲頂點信息 }VertexNode, VertexList[MAXVEX]; typedef struct EdgeNode //邊集表結點 { int begin, end, weight; }EdgeNode, EdgeList[MAXVEX]; typedef struct { VertexList vexList; //頂點表 EdgeList edgeList; //邊集表 int numVertexes, numEdges; //圖中所存儲的頂點數和邊數 }EdgeGraphList; void CreateEdgeGraph(EdgeGraphList* G) { int i, j ,k,w; printf("please input number of vertex and edge:\n"); scanf("%d,%d",&G->numVertexes,&G->numEdges); //輸入頂點數和邊數 getchar(); //可以獲取回車符 //獲取頂點數組信息 for (i = 0; i < G->numVertexes;i++) //輸入頂點信息 { scanf("%c", &G->vexList[i].data); //輸入頂點信息 } //獲取邊數組 for (k = 0; k < G->numEdges;k++) { printf("input edge(vi,vj) vertexs series and the weight:\n"); scanf("%d,%d,%d", &i, &j,&w); getchar(); //由於是無向圖,對稱矩陣,當我們設置邊以后,需要在兩個地方設置結點 G->edgeList[k].begin = i; G->edgeList[k].end = j; G->edgeList[k].weight = w; } } int main() { EdgeGraphList gl; CreateEdgeGraph(&gl); gl; system("pause"); return 0; }



