數據結構 (八) 圖及其存儲

8.1 圖的基本概念 html

8.1.1 圖的定義和術語


1.圖的定義

圖(Graph)是由非空的頂點集合和一個描述頂點之間關係――邊(或者弧)的集合組成,其形式化定義爲:
G=(V,E)
V={vi| vi∈dataobject}
E={( vi,vj)| vi, vj ∈V ∧P(vi, vj)}
其中,G 表示一個圖,V 是圖G 中頂點的集合,E 是圖G 中邊的集合,集合E 中P(vi,vj)表示頂點vi 和頂點vj 之間有一條直接連線,即偶對(vi,vj)表示一條邊。圖8.1 給出了一個圖的示例,在該圖中:
集合V={v1,v2,v3,v4,v5};
集合E={(v1,v2),(v1,v4),(v2,v3),(v3,v4),(v3,v5),(v2,v5)}。
node

2.圖的相關術語

(1)無向圖。在一個圖中,若是任意兩個頂點構成的偶對(vi, vj)∈E 是無序的,即頂點之間的連線是沒有方向的,則稱該圖爲無向圖。如圖8.1 所示是一個無向圖G1。
(2)有向圖。在一個圖中,若是任意兩個頂點構成的偶對(vi, vj)∈E 是有序的,即頂點之間的連線是有方向的,則稱該圖爲有向圖。如圖8.2 所示是一個有向圖G2:
G2=(V2,E2)
V2={v1,v2,v3,v4}
E2={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>}
(3)頂點、邊、弧、弧頭、弧尾。圖中,數據元素vi 稱爲頂點(vertex );P(vi, vj)表示在頂點vi 和頂點vj 之間有一條直接連線。若是是在無向圖中,則稱這條連線爲邊;若是是在有向圖中,通常稱這條連線爲弧。邊用頂點的無序偶對(vi, vj)來表示,稱頂點vi和頂點vj 互爲鄰接點,邊(vi, vj)依附於頂點vi 與頂點vj;弧用頂點的有序偶對<vi, vj>來表示,有序偶對的第一個結點vi 被稱爲始點(或弧尾),在圖中就是不帶箭頭的一端;有序偶對的第二個結點vj 被稱爲終點(或弧頭),在圖中就是帶箭頭的一端。
(4)無向徹底圖。在一個無向圖中,若是任意兩頂點都有一條直接邊相鏈接,則稱該圖爲無向徹底圖。能夠證實,在一個含有n 個頂點的無向徹底圖中,有n(n-1)/2 條邊。
(5)有向徹底圖。在一個有向圖中,若是任意兩頂點之間都有方向互爲相反的兩條弧相鏈接,則稱該圖爲有向徹底圖。在一個含有n 個頂點的有向徹底圖中,有n(n-1)條邊。
(6)稠密圖、稀疏圖。若一個圖接近徹底圖,稱爲稠密圖;稱邊數不多的圖爲稀疏圖。
(7)頂點的度、入度、出度。頂點的度(degree)是指依附於某頂點v 的邊數,一般記爲TD (v)。在有向圖中,要區別頂點的入度與出度的概念。頂點v 的入度是指以頂點爲終點的弧的數目。記爲ID (v);頂點v 出度是指以頂點v 爲始點的弧的數目,記爲OD (v)。
有TD (v)=ID (v)+OD (v)。
例如,在G1 中有:
TD(v1)=2 TD(v2)=3 TD(v3)=3 TD(v4)=2 TD(v5)=2
在G2 中有:
ID(v1)=1 OD(v1)=2 TD(v1)=3
ID(v2)=1 OD(v2)=0 TD(v2)=1
ID(v3)=1 OD(v3)=1 TD(v3)=2
ID(v4)=1 OD(v4)=1 TD(v4)=2

能夠證實,對於具備n 個頂點、e 條邊的圖,頂點vi 的度TD (vi)與頂點的個數以及邊的數目知足關係:
算法

(8)邊的權、網圖。與邊有關的數據信息稱爲權(weight)。在實際應用中,權值能夠有某種含義。好比,在一個反映城市交通線路的圖中,邊上的權值能夠表示該條線路的長度或者等級;對於一個電子線路圖,邊上的權值能夠表示兩個端點之間的電阻、電流或電壓值;對於反映工程進度的圖而言,邊上的權值能夠表示從前一個工程到後一個工程所須要的時間等等。邊上帶權的圖稱爲網圖或網絡(network)。如圖8.3 所示,就是一個無向網圖。若是邊是有方向的帶權圖,則就是一個有向網圖。
(9)路徑、路徑長度。頂點vp 到頂點vq 之間的路徑(path)是指頂點序列vp,vi1,vi2, …,vim,vq.。其中,(vp,vi1),(vi1,vi2),…,(vim,.vq)分別爲圖中的邊。路徑上邊的數目稱爲路徑長度。圖8.1 所示的無向圖G1 中,v1→v4→v3→v5 與v1→v2→v5 是從頂點v1 到頂點v5 的兩條路徑,路徑長度分別爲3 和2。
(10)迴路、簡單路徑、簡單迴路。稱vi 的路徑爲迴路或者環(cycle)。序列中頂點不重複出現的路徑稱爲簡單路徑。在圖8.1 中,前面提到的v1 到v5 的兩條路徑都爲簡單路徑。除第一個頂點與最後一個頂點以外,其餘頂點不重複出現的迴路稱爲簡單迴路,或者簡單環。如圖8.2 中的v1→v3→v4→v1。
(11)子圖。對於圖G=(V,E),G’=(V’,E’),若存在V’是V 的子集,E’是E的子集,則稱圖G’是G 的一個子圖。圖8.4 示出了G2 和G1 的兩個子圖G’和G’’。
vim

(12)連通的、連通圖、連通份量。在無向圖中,若是從一個頂點vi 到另外一個頂點vj(i≠j)有路徑,則稱頂點vi 和vj 是連通的。若是圖中任意兩頂點都是連通的,則稱該圖是連通圖。無向圖的極大連通子圖稱爲連通份量。圖8.5 (a)中有兩個連通份量,如圖8.5 (b)所示。
(13)強連通圖、強連通份量。對於有向圖來講,若圖中任意一對頂點vi 和vj(i≠j)均有從一個頂點vi 到另外一個頂點vj 有路徑,也有從vj 到vi 的路徑,則稱該有向圖是強連通圖。有向圖的極大強連通子圖稱爲強連通份量。圖8.2 中有兩個強連通份量,分別是{v1,v2,v3}和{v4},如圖8.6 所示。
(14)生成樹。所謂連通圖G 的生成樹,是G 的包含其所有n 個頂點的一個極小連通子圖。它一定包含且僅包含G 的n-1 條邊。圖8.4(b)G」示出了圖8.1(a)中G1 的一棵生成樹。在生成樹中添加任意一條屬於原圖中的邊一定會產生迴路,由於新添加的邊使其所依附的兩個頂點之間有了第二條路徑。若生成樹中減小任意一條邊,則必然成爲非連通的。
(15)生成森林。在非連通圖中,由每一個連通份量均可獲得一個極小連通子圖,即一棵生成樹。這些連通份量的生成樹就組成了一個非連通圖的生成森林。
數組

8.1.2 圖的基本操做


(1) CreatGraph(G)輸入圖G 的頂點和邊,創建圖G 的存儲。
(2)DestroyGraph(G)釋放圖G 佔用的存儲空間。
(3)GetVex(G,v)在圖G 中找到頂點v,並返回頂點v 的相關信息。
(4)PutVex(G,v,value)在圖G 中找到頂點v,並將value 值賦給頂點v。
(5)InsertVex(G,v)在圖G 中增添新頂點v。
(6)DeleteVex(G,v)在圖G 中,刪除頂點v 以及全部和頂點v 相關聯的邊或弧。
(7)InsertArc(G,v,w)在圖G 中增添一條從頂點v 到頂點w 的邊或弧。
(8)DeleteArc(G,v,w)在圖G 中刪除一條從頂點v 到頂點w 的邊或弧。
(9)DFSTraverse(G,v)在圖G 中,從頂點v 出發深度優先遍歷圖G。
(10)BFSTtaverse(G,v)在圖G 中,從頂點v 出發廣度優先遍歷圖G。
在一個圖中,頂點是沒有前後次序的,但當採用某一種肯定的存儲方式存儲後,存儲結構中頂點的存儲次序構成了頂點之間的相對次序,這裏用頂點在圖中的位置表示該頂點的存儲順序;一樣的道理,對一個頂點的全部鄰接點,採用該頂點的第i 個鄰接點表示與該頂點相鄰接的某個頂點的存儲順序,在這種意義下,圖的基本操做還有:

(11)LocateVex(G,u)在圖G 中找到頂點u,返回該頂點在圖中位置。
(12)FirstAdjVex(G,v)在圖G 中,返回v 的第一個鄰接點。若頂點在G 中沒有鄰接頂點,則返回「空」。
(13)NextAdjVex(G,v,w)在圖G 中,返回v 的(相對於w 的)下一個鄰接頂點。若w 是v 的最後一個鄰接點,則返回「空」。
網絡

8.2 圖的存儲表示—鄰接矩陣 數據結構

圖是一種結構複雜的數據結構,表如今不只各個頂點的度能夠千差萬別,並且頂點之間的邏輯關係也錯綜複雜。從圖的定義可知,一個圖的信息包括兩部分,即圖中頂點的信息以及描述頂點之間的關係――邊或者弧的信息。所以不管採用什麼方法創建圖的存儲結構,都要完整、準確地反映這兩方面的信息。

下面介紹幾種經常使用的圖的存儲結構。

所謂鄰接矩陣(Adjacency Matrix)的存儲結構,就是用一維數組存儲圖中頂點的信息,用矩陣表示圖中各頂點之間的鄰接關係。假設圖G=(V,E)有n 個肯定的頂點,即V={v0,v1,…,vn-1},則表示G 中各頂點相鄰關係爲一個n×n 的矩陣,矩陣的元素爲:
工具


其中,wij 表示邊(vi,vj)或<vi,vj>上的權值;∞表示一個計算機容許的、大於全部邊上權值的數。
用鄰接矩陣表示法表示圖如圖8.7 所示。
用鄰接矩陣表示法表示網圖如圖8.8 所示。
spa


從圖的鄰接矩陣存儲方法容易看出這種表示具備如下特色:
① 無向圖的鄰接矩陣必定是一個對稱矩陣。所以,在具體存放鄰接矩陣時只需存放上(或下)三角矩陣的元素便可。
② 對於無向圖,鄰接矩陣的第i 行(或第i 列)非零元素(或非∞元素)的個數正好是第i 個頂點的度TD(vi)。
③ 對於有向圖,鄰接矩陣的第i 行(或第i 列)非零元素(或非∞元素)的個數正好是第i 個頂點的出度OD(vi)(或入度ID(vi))。
④用鄰接矩陣方法存儲圖,很容易肯定圖中任意兩個頂點之間是否有邊相連;可是,要肯定圖中有多少條邊,則必須按行、按列對每一個元素進行檢測,所花費的時間代價很大。

這是用鄰接矩陣存儲圖的侷限性。

下面介紹圖的鄰接矩陣存儲表示。

在用鄰接矩陣存儲圖時,除了用一個二維數組存儲用於表示頂點間相鄰關係的鄰接矩陣外,還需用一個一維數組來存儲頂點信息,另外還有圖的頂點數和邊數。故可將其形式描述以下:
#define MaxVertexNum 100 /*最大頂點數設爲100*/
typedef char VertexType; /*頂點類型設爲字符型*/
typedef int EdgeType; /*邊的權值設爲整型*/
typedef struct {
VertexType vexs[MaxVertexNum]; /*頂點表*/
EdeType edges[MaxVertexNum][MaxVertexNum]; /*鄰接矩陣,即邊表*/
int n,e; /*頂點數和邊數*/
}Mgragh; /*Maragh 是以鄰接矩陣存儲的圖類型*/
創建一個圖的鄰接矩陣存儲的算法以下:
void CreateMGraph(MGraph *G)
{/*創建有向圖G 的鄰接矩陣存儲*/
int i,j,k,w;
char ch;
printf("請輸入頂點數和邊數(輸入格式爲:頂點數,邊數):\n");
scanf("%d,%d",&(G->n),&(G->e));/*輸入頂點數和邊數*/
printf("請輸入頂點信息(輸入格式爲:頂點號<CR>):\n");
for (i=0;i<G->n;i++) scanf("\n%c",&(G->vexs[i])); /*輸入頂點信息,創建頂點表*/
for (i=0;i<G->n;i++)
for (j=0;j<G->n;j++) G->edges[i][j]=0; /*初始化鄰接矩陣*/
printf("請輸入每條邊對應的兩個頂點的序號(輸入格式爲:i,j):\n");
for (k=0;k<G->e;k++)
{scanf("\n%d,%d",&i,&j); /*輸入e 條邊,創建鄰接矩陣*/
G->edges[i][j]=1; /*若加入G->edges[j][i]=1;,*/
/*則爲無向圖的鄰接矩陣存儲創建*/
}
}/*CreateMGraph*/
指針

8.2 圖的存儲表示—鄰接表

鄰接表(Adjacency List)是圖的一種順序存儲與鏈式存儲結合的存儲方法。鄰接表表示法相似於樹的孩子鏈表表示法。就是對於圖G 中的每一個頂點vi,將全部鄰接於vi 的頂點vj 鏈成一個單鏈表,這個單鏈表就稱爲頂點vi 的鄰接表,再將全部點的鄰接表表頭放到數組中,就構成了圖的鄰接表。在鄰接表表示中有兩種結點結構,如圖8.9 所示。

一種是頂點表的結點結構,它由頂點域(vertex)和指向第一條鄰接邊的指針域(firstedge)構成,另外一種是邊表(即鄰接表)結點,它由鄰接點域(adjvex)和指向下一條鄰接邊的指針域(next)構成。對於網圖的邊表需再增設一個存儲邊上信息(如權值等)的域(info),網圖的邊表結構如圖8.10 所示。


鄰接表表示的形式描述以下:
#define MaxVerNum 100 /*最大頂點數爲100*/
typedef struct node{ /*邊表結點*/
int adjvex; /*鄰接點域*/
struct node * next; /*指向下一個鄰接點的指針域*/
/*若要表示邊上信息,則應增長一個數據域info*/
}EdgeNode;
typedef struct vnode{ /*頂點表結點*/
VertexType vertex; /*頂點域*/
EdgeNode * firstedge; /*邊表頭指針*/
}VertexNode;
typedef VertexNode AdjList[MaxVertexNum]; /*AdjList 是鄰接表類型*/
typedef struct{
AdjList adjlist; /*鄰接表*/
int n,e; /*頂點數和邊數*/
}ALGraph; /*ALGraph 是以鄰接表方式存儲的圖類型*/
創建一個有向圖的鄰接表存儲的算法以下:
void CreateALGraph(ALGraph *G)
{/*創建有向圖的鄰接表存儲*/
int i,j,k;
EdgeNode * s;
printf("請輸入頂點數和邊數(輸入格式爲:頂點數,邊數):\n");
scanf("%d,%d",&(G->n),&(G->e)); /*讀入頂點數和邊數*/
printf("請輸入頂點信息(輸入格式爲:頂點號<CR>):\n");
for (i=0;i<G->n;i++) /*創建有n 個頂點的頂點表*/
{scanf("\n%c",&(G->adjlist[i].vertex)); /*讀入頂點信息*/
G->adjlist[i].firstedge=NULL; /*頂點的邊表頭指針設爲空*/
}
printf("請輸入邊的信息(輸入格式爲:i,j):\n");
for (k=0;k<G->e;k++) /*創建邊表*/
{scanf("\n%d,%d",&i,&j); /*讀入邊<Vi,Vj>的頂點對應序號*/
s=(EdgeNode*)malloc(sizeof(EdgeNode)); /*生成新邊表結點s*/
s->adjvex=j; /*鄰接點序號爲j*/
s->next=G->adjlist[i].firstedge; /*將新邊表結點s 插入到頂點Vi 的邊表頭部*/
G->adjlist[i].firstedge=s;
}
}/*CreateALGraph*/
算法8.2

若無向圖中有n 個頂點、e 條邊,則它的鄰接表需n 個頭結點和2e 個表結點。顯然,在邊稀疏(e<<n(n-1)/2)的狀況下,用鄰接表表示圖比鄰接矩陣節省存儲空間,當和邊相關的信息較多時更是如此。

在無向圖的鄰接表中,頂點vi 的度恰爲第i 個鏈表中的結點數;而在有向圖中,第i個鏈表中的結點個數只是頂點vi 的出度,爲求入度,必須遍歷整個鄰接表。在全部鏈表中其鄰接點域的值爲i 的結點的個數是頂點vi 的入度。有時,爲了便於肯定頂點的入度或以頂點vi 爲頭的弧,能夠創建一個有向圖的逆鄰接表,即對每一個頂點vi 創建一個連接以vi爲頭的弧的鏈表。例如圖8.12 所示爲有向圖G2(圖8.2)的鄰接表和逆鄰接表。

在創建鄰接表或逆鄰接表時,若輸入的頂點信息即爲頂點的編號,則創建鄰接表的複雜度爲O(n+e),不然,須要經過查找才能獲得頂點在圖中位置,則時間複雜度爲O(n·e)。

在鄰接表上容易找到任一頂點的第一個鄰接點和下一個鄰接點,但要斷定任意兩個頂點(vi 和vj)之間是否有邊或弧相連,則需搜索第i 個或第j 個鏈表,所以,不及鄰接矩陣方便。

8.2 圖的存儲表示—十字鏈表

 十字鏈表(Orthogonal List)是有向圖的一種存儲方法,它其實是鄰接表與逆鄰接表的結合,即把每一條邊的邊結點分別組織到以弧尾頂點爲頭結點的鏈表和以弧頭頂點爲頭頂點的鏈表中。在十字鏈表表示中,頂點表和邊表的結點結構分別如圖8.13 的(a)和(b)所示。


在弧結點中有五個域:其中尾域(tailvex)和頭(headvex)分別指示弧尾和弧頭這兩個頂點在圖中的位置,鏈域hlink 指向弧頭相同的下一條弧,鏈域tlink 指向弧尾相同的下一條弧,info 域指向該弧的相關信息。弧頭相同的弧在同一鏈表上,弧尾相同的弧也在同一鏈表上。它們的頭結點即爲頂點結點,它由三個域組成:其中vertex 域存儲和頂點相關的信息,如頂點的名稱等;firstin 和firstout 爲兩個鏈域,分別指向以該頂點爲弧頭或弧尾的第一個弧結點。例如,圖8.14(a)中所示圖的十字鏈表如圖8.14(b)所示。若將有向圖的鄰接矩陣當作是稀疏矩陣的話,則十字鏈表也能夠當作是鄰接矩陣的鏈表存儲結構,在圖的十字鏈表中,弧結點所在的鏈表非循環鏈表,結點之間相對位置天然造成,不必定按頂點序號有序,表頭結點即頂點結點,它們之間而是順序存儲。有向圖的十字鏈表存儲表示的形式描述以下:
#define MAX_VERTEX_NUM 20
typedef struct ArcBox {
int tailvex,headvex; /*該弧的尾和頭頂點的位置*/
struct ArcBox * hlink, tlink; /分別爲弧頭相同和弧尾相財的弧的鏈域*/
InfoType info; /*該弧相關信息的指針*/
}ArcBox;
typedef struct VexNode {
VertexType vertex:
ArcBox fisrin, firstout; /*分別指向該頂點第一條入弧和出弧*/
}VexNode;
typedef struct {
VexNode xlist[MAX_VERTEX_NUM]; /*表頭向量*/
int vexnum,arcnum; /*有向圖的頂點數和弧數*/
}OLGraph;


下面給出創建一個有向圖的十字鏈表存儲的算法。經過該算法,只要輸入n 個頂點的信息和e 條弧的信息,即可創建該有向圖的十字鏈表,其算法內容以下。
void CreateDG(LOGraph **G)
/*採用十字鏈表表示,構造有向圖G(G.kind=DG)*/
{ scanf (&(*G->brcnum),&(*G->arcnum),&IncInfo); /*IncInfo 爲0 則各弧不含其實信息*/
for (i=0;i<*G->vexnum;++i) /*構造表頭向量*/
{ scanf(&(G->xlist[i].vertex)); /*輸入頂點值*/
*G->xlist[i].firstin=NulL;*G->xlist[i].firstout =NULL; /*初始化指針*/
}
for(k=0;k<G.arcnum;++k) /*輸入各弧並構造十字鏈表*/
{ scanf(&v1,&v2); /*輸入一條弧的始點和終點*/
i=LocateVex(*G,v1); j=LocateVex(*G,v2); /*肯定v1 和v2 在G 中位置*/
p=(ArcBox*) malloc (sizeof(ArcBox)); /*假定有足夠空間*/
*p={ i,j,*G->xlist[j].fistin,*G->xlist[i].firstout,NULL} /*對弧結點賦值*/
/*{tailvex,headvex,hlink,tlink,info}*/
*G->xlist[j].fisrtin=*G->xlist[i].firstout=p; /*完成在入弧和出弧鏈頭的插入*/
if (IncInfo) Input( p->info); /*若弧含有相關信息,則輸入*/
}
}/*CreateDG*/
算法8.3

在十字鏈表中既容易找到覺得尾的弧,也容易找到以vi 爲頭的弧,於是容易求得頂點的出度和入度(或須要,可在創建十字鏈表的同時求出)。同時,由算法8.3 可知,創建十字鏈表的時間複雜度和創建鄰接表是相同的。在某些有向圖的應用中,十字鏈表是頗有用的工具。

8.2 圖的存儲表示—鄰接多重表

鄰接多重表(Adjacency Multilist)主要用於存儲無向圖。由於,若是用鄰接表存儲無向圖,每條邊的兩個邊結點分別在以該邊所依附的兩個頂點爲頭結點的鏈表中,這給圖的某些操做帶來不便。例如,對已訪問過的邊作標記,或者要刪除圖中某一條邊等,都須要找到表示同一條邊的兩個結點。所以,在進行這一類操做的無向圖的問題中採用鄰接多重表做存儲結構更爲適宜。

鄰接多重表的存儲結構和十字鏈表相似,也是由頂點表和邊表組成,每一條邊用一個結點表示,其頂點表結點結構和邊表結點結構如圖8.15 所示。


其中,頂點表由兩個域組成,vertex 域存儲和該頂點相關的信息firstedge 域指示第一條依附於該頂點的邊;邊表結點由六個域組成,mark 爲標記域,可用以標記該條邊是否被搜索過;ivex 和jvex 爲該邊依附的兩個頂點在圖中的位置;ilink 指向下一條依附於頂點ivex的邊;jlink 指向下一條依附於頂點jvex 的邊,info 爲指向和邊相關的各類信息的指針域。


例如,圖8.16 所示爲無向圖8.1 的鄰接多重表。在鄰接多重表中,全部依附於同一頂點的邊串聯在同一鏈表中,因爲每條邊依附於兩個頂點,則每一個邊結點同時連接在兩個鏈表中。可見,對無向圖而言,其鄰接多重表和鄰接表的差異,僅僅在於同一條邊在鄰接表中用兩個結點表示,而在鄰接多重表中只有一個結點。所以,除了在邊結點中增長一個標誌域外,鄰接多重表所需的存儲量和鄰接表相同。在鄰接多重表上,各類基本操做的實現亦和鄰接表類似。鄰接多重表存儲表示的形式描述以下:#define MAX_VERTEX_NUM 20typedef emnu{ unvisited,visited} VisitIf;typedef struct EBox{VisitIf mark: /*訪問標記*/int ivex,jvex; /*該邊依附的兩個頂點的位置*/struct EBox ilink, jlink; /*分別指向依附這兩個頂點的下一條邊*/InfoType info; /*該邊信息指針*/}EBox;typedef struct VexBox{VertexType data;EBox fistedge; /*指向第一條依附該頂點的邊*/}VexBox;typedef struct{VexBox adjmulist[MAX_VERTEX_NUM];int vexnum,edgenum; /*無向圖的當前頂點數和邊數*/}AMLGraph;

相關文章
相關標籤/搜索