上一節介紹瞭如何使用順序存儲結構存儲圖,而在實際應用中最常用的是本節所介紹的鏈式存儲結構:圖中每個頂點作爲鏈表中的結點,結點的構成分爲數據域和指針域,數據域存儲圖中各頂點中存儲的數據,而指針域負責表示頂點之間的關聯。
使用鏈式存儲結構表示圖的常用方法有 3 種:鄰接表
、鄰接多重表
和十字鏈表
。
鄰接的意思是頂點之間有邊或者弧存在,通過當前頂點,可以直接找到下一個頂點。
鄰接表
使用鄰接表存儲圖時,對於圖中的每一個頂點和它相關的鄰接點,都存儲到一個鏈表中。每個鏈表都配有頭結點,頭結點的數據域不爲NULL,而是用於存儲頂點本身的數據;後續鏈表中的各個結點存儲的是當前頂點的所有鄰接點。
所以,採用鄰接表存儲圖時,有多少頂點就會構建多少個鏈表,爲了便於管理這些鏈表,常用的方法是將所有鏈表的鏈表頭按照一定的順序存儲在一個數組中(也可以用鏈表串起來)。
總結起來,鄰接表的處理方法是這樣:
- 圖中頂點用一個一維數組存儲,當然,頂點也可以用單鏈表來存儲,不過數組可以較容易地讀取頂點信息,更加方便。
- 圖中每個頂點 Vi 的所有鄰接點構成一個線性表,由於鄰接點的個數不確定,所以我們選擇用單鏈表來存儲。
在鄰接表中,每個鏈表的頭結點和其它結點的組成成分有略微的不同。各自的結構構成如下圖所示:
表頭結點結構:
data
域存儲該頂點含有的數據;firstarc
爲指針域,指向當前頂點的首個鄰接點。
其他節點結構:
adjvex
存儲鄰接點在數組中的位置下標;nextarc
指向下一個結點(鄰接點)的指針;info
記錄權值的信息域。
info 域對於無向圖來說,本身不具備權值和其它相關信息,就可以根據需要將之刪除。
例如,當存儲圖 2(A)所示的有向圖時,構建的鄰接表如圖 2(B)所示:
鄰接表存儲圖的存儲結構爲:
#define MAX_VERTEX_NUM 20 //最大頂點個數
#define VertexType int //頂點數據的類型
#define InfoType int //圖中弧或者邊包含的信息的類型
typedef struct ArcNode{
int adjvex; //鄰接點在數組中的位置下標
struct ArcNode * nextarc;//指向下一個鄰接點的指針
InfoType * info; //信息域
}ArcNode;
typedef struct VNode{
VertexType data; //頂點的數據域
ArcNode * firstarc; //指向鄰接點的指針
}VNode,AdjList[MAX_VERTEX_NUM];//存儲各鏈表頭結點的數組
typedef struct {
AdjList vertices; //圖中頂點及各鄰接點數組
int vexnum,arcnum; //記錄圖中頂點數和邊或弧數
int kind; //記錄圖的種類
}ALGraph;
鄰接表計算頂點的度
使用鄰接表存儲無向圖時,各頂點的度
爲各自鏈表中包含的結點數;存儲有向圖時,各自鏈表中具備的結點數爲該頂點的出度
。求入度時,需要遍歷整個鄰接表中的結點,統計數據域和該頂點數據域相同的結點的個數,即爲頂點的入度。
對於求有向圖中某結點的入度,還有一種方法就是再建立一個逆鄰接表
,此表只用於存儲圖中每個指向該頂點的所有的頂點在數組中的位置下標。例如,構建圖 2(A)的逆鄰接表,結果爲:
對於具有 n 個頂點和 e 條邊的無向圖,鄰接表中需要存儲 n 個頭結點和 2e 個表結點。在圖中邊或者弧稀疏的時候,使用鄰接表要比前一節介紹的鄰接矩陣更加節省空間。
十字鏈表
十字鏈表存儲的對象是有向圖或者有向網
。同鄰接表相同的是,圖(網)中每個頂點各自構成一個鏈表,爲鏈表的首元結點。同時,對於有向圖(網)中的弧來說,有弧頭和弧尾。一個頂點所有的弧頭的數量即爲該頂點的入度,弧尾的數量即爲該頂點的出度。每個頂點構成的鏈表中,以該頂點作爲弧頭的弧單獨構成一個鏈表,以該頂點作爲弧尾的弧也單獨構成一個鏈表,兩個鏈表的表頭都爲該頂點構成的頭結點。
這樣,由每個頂點構建的鏈表按照一定的順序存儲在數組中,就構成了十字鏈表
。
所以,十字鏈表中由兩種結點構成:頂點結點
和弧結點
。各自的結構構成如下圖所示:
弧結點結構:
tailvex
和headvex
分別存儲的是弧尾和弧頭對應的頂點在數組中的位置下標;hlink
和tlink
爲指針域,分別指向弧頭相同的下一個弧和弧尾相同的下一個弧;info
爲指針域,存儲的是該弧具有的相關信息,例如權值等。
頂點結點結構(圖中頂點也是用一個一維數組存儲,只是爲了方便畫線沒有連在一起):
data
域存儲該頂點含有的數據;firstin
和firstout
爲兩個指針域,分別指向以該頂點爲弧頭和弧尾的首個弧結點。
例如,使用十字鏈表存儲有向圖 5(A) ,構建的十字鏈表如圖 (B) 所示,構建代碼實現爲:
#define MAX_VERTEX_NUM 20
#define InfoType int //圖中弧包含信息的數據類型
#define VertexType int
typedef struct ArcBox{
int tailvex,headvex; //弧尾、弧頭對應頂點在數組中的位置下標
struct ArcBox *hlik,*tlink;//分別指向弧頭相同和弧尾相同的下一個弧
InfoType *info; //存儲弧相關信息的指針
}ArcBox;
typedef struct VexNode{
VertexType data; //頂點的數據域
ArcBox *firstin,*firstout;//指向以該頂點爲弧頭和弧尾的鏈表首個結點
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM];//存儲頂點的一維數組
int vexnum,arcnum; //記錄圖的頂點數和弧數
}OLGraph;
int LocateVex(OLGraph * G,VertexType v){
int i=0;
//遍歷一維數組,找到變量v
for (; i<G->vexnum; i++) {
if (G->xlist[i].data==v) {
break;
}
}
//如果找不到,輸出提示語句,返回 -1
if (i>G->vexnum) {
printf("no such vertex.\n");
return -1;
}
return i;
}
//構建十字鏈表函數
void CreateDG(OLGraph *G){
//輸入有向圖的頂點數和弧數
scanf("%d,%d",&(G->vexnum),&(G->arcnum));
//使用一維數組存儲頂點數據,初始化指針域爲NULL
for (int i=0; i<G->vexnum; i++) {
scanf("%d",&(G->xlist[i].data));
G->xlist[i].firstin=NULL;
G->xlist[i].firstout=NULL;
}
//構建十字鏈表
for (int k=0;k<G->arcnum; k++) {
int v1,v2;
scanf("%d,%d",&v1,&v2);
//確定v1、v2在數組中的位置下標
int i=LocateVex(G, v1);
int j=LocateVex(G, v2);
//建立弧的結點
ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));
p->tailvex=i; //存儲弧尾對應的頂點在數組中的位置下標
p->headvex=j; //存儲弧頭對應的頂點在數組中的位置下標
//採用頭插法插入新的p結點
p->hlik=G->xlist[j].firstin;//指向弧頭相同的下一個弧;
p->tlink=G->xlist[i].firstout;//指向弧尾相同的下一個弧;
G->xlist[j].firstin=G->xlist[i].firstout=p;
}
}
對於鏈表中的各個結點來說,由於表示的都是該頂點的出度或者入度,所以結點之間沒有先後次序之分,程序中構建鏈表對於每個新初始化的結點採用頭插法進行插入。
頭插法
: 在鏈表的開頭插入一個新的節點,也就是,必須使得鏈表頭Head指向新節點,該新節點指向原來是表頭的第一個節點Node newNode; //生成新節點newNode Node curr = head.next; newNode.next = curr; //新節點指向原來的第一節點 head.next = newNode; //頭節點指向新節點
十字鏈表計算頂點的度
採用十字鏈表表示的有向圖,在計算某頂點的出度時,爲 firstout 域鏈表中結點的個數;入度爲 firstin 域鏈表中結點的個數。
鄰接多重表
使用鄰接表解決在無向圖
中刪除某兩個結點之間的邊的操作時,由於表示邊的結點分別處在兩個頂點爲頭結點的鏈表中,所以需要都找到並刪除,操作比較麻煩。處理類似這種操作,使用鄰接多重表會更合適。
例如,若要刪除(V0,V2)這條邊,就需要對鄰接表結構中邊表的兩個結點進行刪除操作。
鄰接多重表可以看做是鄰接表
和十字鏈表
的結合體。和十字鏈表唯一不同的是頂點結點和表結點的結構組成不同;同鄰接表相比,不同的地方在於鄰接表表示無向圖中每個邊都用兩個結點,分別在兩個不同鏈表中;而鄰接多重表表示無向圖中的每個邊只用一個結點。
鄰接多重表的頂點結點和表結點的構成如圖 6 所示:
表結點構成:
mark
爲標誌域,作用是標記某結點是否已經被操作過,例如在遍歷各結點時, mark 域爲 0 表示還未遍歷;mark 域爲 1 表示該結點遍歷過;ivex
和jvex
分別表示該結點表示的邊兩端的頂點在數組中的位置下標;ilink
指向下一條與 ivex 相關的邊(或依附頂點 ivex 的下一條邊);jlink
指向下一條與 jvex 相關的邊(或依附頂點 jvex 的下一條邊);info
指向與該邊相關的信息。
頂點結點構成:
data
爲該頂點的數據域;firstedge
爲指向第一條跟該頂點有關係的邊。
例如,使用鄰接多重表表示圖 7中左邊的無向圖時,與之相對應的鄰接多重表如圖右側所示。
鄰接多重表的存儲結構用代碼表示爲:
#define MAX_VERTEX_NUM 20 //圖中頂點的最大個數
#define InfoType int //邊含有的信息域的數據類型
#define VertexType int //圖頂點的數據類型
typedef enum {unvisited,visited}VisitIf; //邊標誌域
typedef struct EBox{
VisitIf mark; //標誌域
int ivex,jvex; //邊兩邊頂點在數組中的位置下標
struct EBox * ilink,*jlink; //分別指向與ivex、jvex相關的下一個邊
InfoType *info; //邊包含的其它的信息域的指針
}EBox;
typedef struct VexBox{
VertexType data; //頂點數據域
EBox * firstedge; //頂點相關的第一條邊的指針域
}VexBox;
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM]; //存儲圖中頂點的數組
int vexnum,degenum; //記錄途中頂點個數和邊個數的變量
}AMLGraph;
總結
本節介紹了有關圖的三種鏈式存儲結構:鄰接表、十字鏈表和鄰接多重表。
鄰接表
適用於所有的圖結構,無論是有向圖(網)還是無向圖(網),存儲結構較爲簡單,但是在存儲一些問題時,例如計算某頂點的度,需要通過遍歷的方式自己求得。
十字鏈表
適用於有向圖(網)的存儲,使用該方式存儲的有向圖,可以很容易計算出頂點的出度和入度,只需要知道對應鏈表中的結點個數即可。
鄰接多重表
適用於無向圖(網)的存儲,該方式避免了使用鄰接表存儲無向圖時出現的存儲空間浪費的現象,同時相比鄰接表存儲無向圖,更方便了某些邊操作(遍歷、刪除等)的實現。