資料結構--圖的儲存方式
主要寫一下圖的幾種表示方法。
今天寫的幾種圖儲存結構包括鄰接矩陣,鄰接表,十字連結串列,鄰接多重表,邊集陣列。
主要是鄰接表,十字連結串列和鄰接多重表。鄰接矩陣和邊集陣列比較好理解和實現
我在網上查了一些資料和看了課本上的十字連結串列和鄰接多重表的例子,個人覺得講的不那麼通俗,尤其是課本...
目錄:
0.預備知識
1.鄰接矩陣
2.鄰接表
3.十字連結串列
4.鄰接多重表
5.邊集陣列
0.預備知識
<1.圖表示方法
資料結構中的一個圖是用G = (V, E)集合來表示的, V(vertex)是頂點集合, E(edge)是邊集合
看下圖
頂點直接表示:頂點集合(v1, v2, v3)
我們只能間接的用兩個點來表示一條邊:E((v1, v2), (v1, v3), (v3, v2))
<2.有向和無向圖
下圖的v1-v2是雙通還是隻能有一個方向通過
<3.出度和入度
出度:一個點可以選擇多少條路徑到達其他地方。
入度:有多少條路徑可以到達這個點。
第一張圖v1出度是2,入度是0。
<4.權重:標記一條路徑長短的值。
1.鄰接矩陣
簡單理解就是用一個二維矩陣(二維陣列)來儲存。按照標記來檢視此邊是否存在。鄰接矩陣為每一種情況都做好了空間預存。
該圖是一個有向圖,右圖為鄰接矩陣,我們可以看出,v1通往v2,那麼G[v1][v2]標記為1,v2不能通往v1,G[v2][v1]為0,不存在v1到v1路徑,我們初始化為 “-”。
標記為1為了簡單起見,有權圖我們則標記為權重
適用場景:使用於稠密的圖,可以快速定位到指定的邊,但是如果是稀疏的圖,會比較浪費空間。
程式碼:
#include <stdio.h> #include <malloc.h> #include <assert.h> //const int Maxvex = 100; enum{ Maxvex = 100 }; const int infinity = 10000; typedef struct graph { int vexs[Maxvex+1];//頂點數 int arc[Maxvex+1][Maxvex+1];//鄰接矩陣 int EdgeNum, VexsNum;//邊數,頂點數 }Mgraph; void create_graph(Mgraph *G) { int i,j,k,w; printf("請輸入頂點數,邊數:"); scanf("%d,%d", &G->VexsNum, &G->EdgeNum); //輸入頂點 for(i = 1; i <= G->VexsNum; i++) scanf("%d", &G->vexs[i]); //初始化鄰接矩陣 for(i = 1; i <= G->VexsNum; i++) { for(j = 1; j <= G->VexsNum; j++) { G->arc[i][j] = infinity; } } //錄入邊 printf("EdgeNum:%d\n", G->EdgeNum); for(k = 1; k <= G->EdgeNum; k++) { printf("輸入邊的i, j, 權值:"); scanf("%d,%d,%d", &i, &j, &w); G->arc[i][j] = w; G->arc[j][i] = G->arc[i][j];//無向圖arc[i][j]和 arc[j][i]是同一條邊,有向圖則只賦值一邊即可 } } int main() { Mgraph *G; G = (Mgraph*)malloc(sizeof(Mgraph)); assert(G != NULL);//if G != NULL false error create_graph(G); }
2.鄰接表
上面所說鄰接矩陣它包含了每一種可能出現情況,不適合稀疏圖儲存,所以出現了鄰接表。
右圖為鄰接表,節省空間,儲存方式決定了它只能表示某個頂點的入度或者出度,不能快速定位到某一條邊。
比如v1,有兩條路徑v1 -- v2
v1 -- v3,那麼v1的出度是2,但是我們不能表示它的入度。
右圖的鄰接表可以看出v1指向了v2,v1也指向了v3 (v2,v3之間的箭頭只是為了連結串列連線),表示從v1可以通往v2和v3。
如果我們想表示入度可以建立一個逆鄰接表,來表示入度。比如v2 -- v1,因為v2的入度是1,只有v1通往它。
適用場景:稀疏圖的儲存,節省空間。
程式碼:(要理解頂點集和邊集的結構)
#include <stdio.h>
#include <assert.h>
#include <malloc.h>
enum { verMax = 10 };
typedef int VertexType;
typedef int EdgeType;
typedef struct EdgeNode //邊表節點
{
int adjvex; //節點
EdgeType weight; //權
struct EdgeNode* next;
}EdgeNode;
typedef struct VertexNode //頂點表節點
{
VertexType data;
EdgeNode *firstEdge; //邊表頭指標
}Adjlist[verMax], VertexNode; //Adglist 是 struct Vertex [verMax]型別的
typedef struct AdjGraph
{
int EdgeNum, VertexNum;
Adjlist adjlist;
}AdjGraph;
void Create_graph(AdjGraph *G)
{
int i, j, k, w;
EdgeNode *e;
printf("請輸入頂點,邊數:");
scanf("%d,%d", &G->VertexNum, &G->EdgeNum);
printf("請輸入頂點:");
for(i = 1; i <= G->VertexNum; i++)
{
scanf("%d", &G->adjlist[i].data);
G->adjlist[i].firstEdge = NULL;
}
for(k = 1; k <= G->EdgeNum; k++)
{
printf("請輸入邊的兩端節點和權v1, v2, w:");
scanf("%d,%d,%d", &i, &j, &w);
e = (EdgeNode*)malloc(sizeof(EdgeNode));
assert(e != NULL);
e->adjvex = j;
e->next = G->adjlist[i].firstEdge;
G->adjlist[i].firstEdge = e;
/*
e = (EdgeNode*)malloc(sizeof(EdgeNode)); //註釋為逆鄰接表的建立方式
assert(e != NULL);
e->adjvex = i;
e->next = G->adjlist[j].firstEdge;
G->adjlist[j].firstEdge = e;
*/
}
}
int main()
{
AdjGraph *G;
int i = 0, j = 0;
EdgeNode *e;
G = (AdjGraph *)malloc(sizeof(AdjGraph));
assert(G != NULL);
Create_graph(G);
printf("\n");
for(i = 1; i <= G->VertexNum; i++)
{
printf("|%d|->", G->adjlist[i].data);
e = G->adjlist[i].firstEdge;
while(e != NULL)
{
printf("%d->", e->adjvex);
e = e->next;
}
printf("NULL\n");
}
return 0;
}
3.十字連結串列
鄰接表在某種程度上是有缺陷的,它表示了出度就表示不了入度。
所以出現了十字連結串列,它既能表示入度也能表示出度。
說通俗點,十字連結串列也就是鄰接表的改進,頂點集包含兩個指標,firstIn和firstOut,
firstIn指向入邊表(逆鄰接表), firstOut表示出邊表(也就是鄰接表)。(需要先理解上面程式碼中頂點集和邊節點的結構)。
圖畫的不太好,見諒...
注意右圖的箭頭是指向整個結構。
看右圖的結構:每個節點都有firstIn和firstOut是用來表示入度的邊(fristIn)和出度的邊(firstOut),可以這樣表示是因為
每個邊的儲存結構是右上角包括(起點, next,指向頂點, 反next,和 w),比如左圖的路徑v1 --> v3, 那麼對於v1來說
v3是v1的出度路徑,對於v3來說v1是v3的入度路徑,那麼當我們讀入這條邊的時候,我們就要同時更新決定這條邊的兩個頂點
所以在右圖的十字連結串列其實我們只錄入了 v1 --> v3這一條邊,但是頂點集合v1的firOut和v3的firIn就更新了(右圖的兩個箭頭),
遍歷十字連結串列時,firsIn後面連線的就是該點的入度,firsOut後面連線的就是該點的出度。
這樣每次錄入邊我們能更新入度和出度,錄入完成後就可以同時訪問入度和出度了~
程式碼:(還是需理解頂點集和邊節點的宣告定義)
/*
* 十字連結串列, 結合鄰接表和逆鄰接表的一種資料結構
* 時間複雜度和鄰接表相同
* 因此在有向圖中, 它是很好的資料結構
*/
#include <stdio.h>
#include <assert.h>
#include <malloc.h>
enum { verMax = 10 };
typedef int VertexType;
typedef int EdgeType;//權
//邊表節點結構
typedef struct EdgeNode
{
int adjvex;
int again_adjvex;
struct EdgeNode* again_next;
struct EdgeNode* next;
EdgeType weight;
}EdgeNode;
//頂點表
typedef struct VertexNode
{
VertexType data;
EdgeNode* firstin; //逆鄰接表邊表的下一個節點
EdgeNode* firstout; //鄰接表邊表的下一個節點
}CrossList[verMax+1], VertexNode;
typedef struct CrossGraph
{
int EdgeNum, VertexNum;
CrossList crosslist;
}CrossGraph;
void Create_graph(CrossGraph *G)
{
int i, j, k, w;
EdgeNode *e;
EdgeNode *q;
printf("請輸入頂點,邊數:");
scanf("%d,%d", &G->VertexNum, &G->EdgeNum);
printf("請輸入頂點:");
for(k = 1; k <= G->VertexNum; ++k)//初始化頂點表
{
scanf("%d", &G->crosslist[k].data);
G->crosslist[k].firstin = NULL;
G->crosslist[k].firstout = NULL;
}
for(k = 1; k <= G->EdgeNum; ++k)
{
printf("請輸入邊的兩端節點和權v1, v2, w:");
scanf("%d,%d,%d", &i, &j, &w);
e = (EdgeNode*)malloc(sizeof(EdgeNode));
assert(e != NULL);
e->next = NULL;
e->again_next = NULL;
e->again_adjvex = i;//逆鄰接表邊表節點
e->adjvex = j; //鄰接表
e->next = G->crosslist[i].firstout;
G->crosslist[i].firstout = e;
//比鄰接表多了一個入度的表示
e->again_next = G->crosslist[j].firstin;
G->crosslist[j].firstin = e;
}
}
int main()
{
CrossGraph *G;
int i = 0, j = 0;
EdgeNode *e;
G = (CrossGraph *)malloc(sizeof(CrossGraph));
assert(G != NULL);
Create_graph(G);
printf("鄰接表\n");
for(i = 1; i <= G->VertexNum; ++i)
{
printf("|%d|->", G->crosslist[i].data);
e = G->crosslist[i].firstout;
while(e != NULL)
{
printf("%d->", e->adjvex);
e = e->next;
}
printf("NULL\n");
}
printf("逆鄰接表\n");
for(i = 1; i <= G->VertexNum; ++i)
{
printf("|%d|<-", G->crosslist[i].data);
e = G->crosslist[i].firstin;
while(e != NULL)
{
printf("%d<-", e->again_adjvex);
//標記 , 注意逆鄰接表的標量, 分清next 和 again_next!
e = e->again_next;
}
printf("NULL\n");
}
return 0;
}
4.鄰接多重表
如果我們更加關注邊的操作,且存在刪除邊的操作,那麼鄰接多重表是個不錯的選擇。
上面的結構刪除一個邊要執行多個操作
看下鄰接多重表的表示,重點注意下邊結構的5個組成部分(ivex, ilink, jvex, jlink, weight),稍候解釋
右圖是執行插入邊E1(v1,v3)和邊E2(v3,v4)後鄰接多重表的樣子
先說下插入的過程,然後在解釋為什麼這樣,比較好理解。
1.插入邊E1(v1, v3),此時頂點集合(豎著的表格)指標域都為NULL,因為邊E1是v1和v3點組成, 頂點集v1和v3都要更新,也就是箭頭1和2,都指向了邊E1(v1,v3)。
2.接下來插入邊E2(v3,v4),判斷邊E2(v3,v4)的第一個節點是v3,那麼在頂點集合(v1,v2,v3,v4)中找尋v3,找到了後看v3的指向,如果為NULL則讓
頂點集合的v3的指標域指向(和上面1一樣),不為NULL(此時不為NULL)則一直向後尋找,更新發現jlink為NULL,讓這個NULL指標指向新插入的邊。如右圖的箭頭指
針3,接下來判斷E2邊的第二個組成節點是v4,那麼先看頂點集合v4的指標為NULL,和步驟1一樣直接更新指向E2即可,不用向後尋找。
明白了這個操作之後,說說為什麼。不明白再看下,不是很好理解 ^ _ ^
鄰接多重表是以邊來新增到圖結構中的,所有表示的邊無非依靠的就是圖中的全部頂點而已,那麼頂點集合(上圖豎著的表)其實就是起了一個開頭的作用,
後面所有出現v1節點的邊都會通過頂點集合的v1指標(只有1個,開頭),加上後面的包含v1節點的邊結構中的ilink或者是jlink來連線起來(ilink還是jlink根據
v1是邊的ivex還是jvex)。
還沒理解的話看看頂點集的v3和箭頭2,3,串起來了所有的包含v3的邊。
這樣頂點集加上邊集合我們就能訪問這個圖結構,頂點v1串起來所有包含v1的邊,頂點v2串起來的所有包含v2的邊。
刪除邊的時候我們只需要改變指標ilink和jlink即可,很方便。
程式碼:
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
enum { verMax = 10 };
typedef int VertexType;
typedef int EdgeType;
//邊節點資訊
typedef struct EdgeNode
{
int mark; //標記是否訪問過
int ivex;
int jvex;
struct EdgeNode* ilink;
struct EdgeNode* jlink;
int weight;
}EdgeNode;
//頂點資訊
typedef struct VertexNode
{
VertexType vertex;
EdgeNode* firstEdge;
}Adjmulist[verMax+1], VertexNode;
//圖結構
typedef struct
{
int VertexNum, EdgeNum;
Adjmulist adjmulist;
}adjmulistGraph;
void Print_Graph(adjmulistGraph *G)
{
int i;
EdgeNode* p;
for(i = 1; i <= G->VertexNum; ++i)
{
p = G->adjmulist[i].firstEdge;
while(p != NULL)
{
if(p->ivex == i) //判斷相等才能知道連線上的是ivex還是jvex;
{
printf("%d--%d\n", G->adjmulist[p->ivex].vertex, G->adjmulist[p->jvex].vertex);
p = p->ilink;
}
else//jvex
{
printf("%d--%d\n", G->adjmulist[p->jvex].vertex, G->adjmulist[p->ivex].vertex);
p = p->jlink;
}
}
}
}
void Create_Graph(adjmulistGraph *G)
{
int criculate;
int vertex[2]; //需要測試一條邊的兩個點
EdgeNode *p, *newedge;
int i;
int w;
printf("請輸入頂點,邊數:");
scanf("%d,%d", &G->VertexNum, &G->EdgeNum);
printf("請輸入頂點:");
for(i = 1; i <= G->VertexNum; ++i)
{
scanf("%d", &G->adjmulist[i].vertex);
}
criculate = G->EdgeNum;
while(criculate--)
{
printf("請輸入v1,v2");
scanf("%d,%d,%d", &vertex[0], &vertex[1], &w);
newedge = (EdgeNode*)malloc(sizeof(EdgeNode));
assert(newedge != NULL);
newedge->ivex = vertex[0];
newedge->jvex = vertex[1];
newedge->weight = w;
//組成一條邊的兩個點都要處理
for(i = 0; i < 2; i++)
{
p = G->adjmulist[vertex[i]].firstEdge;
if(p == NULL)
{
G->adjmulist[vertex[i]].firstEdge = newedge;
}
else
{ <pre name="code" class="cpp"> //找到末尾
while((p->ilink != NULL && p->ivex == vertex[i]) || (p->jlink != NULL && p->jvex == vertex[i])) { if(vertex[i] == p->ivex) p = p->ilink; else// ==jvex p = p->jlink; } if(p->ivex == vertex[i]) p->ilink = newedge; else p->jlink
= newedge; } } }}int main(){ adjmulistGraph *G; G = (adjmulistGraph*)malloc(sizeof(adjmulistGraph)); assert(G != NULL); Create_Graph(G); printf("\n\n"); Print_Graph(G); return 0;}
5.邊集陣列
更加關注的是邊的集合且依次對邊處理的操作,但是如果要查詢一個頂點的度,需要掃描整個集合,效率並不高,使用一個二維陣列表示即可。
這個沒什麼好說的,每個邊表示起點終點權重等。
最後放下這幾個圖儲存結構的比較
參考書籍:《大話資料結構》
《資料結構與演算法分析 -- c語言描述》
有問題一起討論哈