資料結構與演演算法(十一):圖的儲存與遍歷
圖的定義
圖(Graph)是由非空的頂點集合和一個描述頂點之間的關係——邊(或者弧)的集合組成的,其形式化定義為: G=(V,E) V={vi | vi∈data object } E={(vi,vj) | vi,vj ∈V∧P (vi,vj)} 其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合,集合E中P(vi,vj)表示頂點vi和頂點vj之間有一條直接連線,即偶對(vi,vj)表示一條邊。
無向圖
在一個圖中,如果任意兩個頂點構成的偶對(vi,vj)是無序的,即頂點之間的連線是沒有方向的,則稱該圖為無向圖。
V={v1,v2,v3,v4,v5};
E={(v1,v2),(v1,v4),(v2,v3),(v3,v5),v5)}
複製程式碼
有向圖
在一個圖中,如果任意兩個頂點構成的偶對<vi,vj>是有序的(有序對常用尖括號“< >”表示),即頂點之間的連線是有方向的,則稱該圖為有向圖。
V={v1,v4}
E={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>}
複製程式碼
頂點、邊、弧、弧頭、弧尾
在圖中,資料元素vi稱為頂點(Vertex);(vi,vj)表示在頂點vi和頂點vj之間有一條直接連線。如果是在無向圖中,則稱這條連線為邊;如果是在有向圖中,一般稱這條連線為弧。
邊用頂點的無序偶對(vi,vj)來表示,稱頂點vi和頂點vj互為鄰接點,邊(vi,vj)依附於頂點vi與頂點vj;
弧用頂點的有序偶對<vi,vj>來表示,有序偶對的第一個結點vi被稱為始點(或弧尾),在圖中就是不帶箭頭的一端;有序偶對的第二個結點vj被稱為終點(或弧頭),在圖中就是帶箭頭的一端。
無向完全圖
在一個無向圖中,如果任意兩頂點都有一條直接邊相連線,則稱該圖為無向完全圖。可以證明,在一個含有n個頂點的無向完全圖中,有n(n-1)/2條邊。
有向完全圖
在一個有向圖中,如果任意兩頂點之間都有方向互為相反的兩條弧相連線,則稱該圖為有向完全圖。在一個含有n個頂點的有向完全圖中,有n(n-1)條邊。
頂點的度、入度、出度
頂點的度(Degree)是指依附於某頂點v的邊數,通常記為TD (v)。在有向圖中,要區別頂點的入度與出度的概念。頂點v的入度是指以頂點v為終點的弧的數目,記為ID(v);頂點v出度是指以頂點v為始點的弧的數目。
邊的權、網
在某些實際場景中,圖中的每條邊(或弧)會賦予一個實數來表示一定的含義,這種與邊(或弧)相匹配的實數被稱為"權",而帶權的圖通常稱為網。
路徑和迴路
無論是無向圖還是有向圖,從一個頂點到另一頂點途徑的所有頂點組成的序列(包含這兩個頂點),稱為一條路徑。如果路徑中第一個頂點和最後一個頂點相同,則此路徑稱為"迴路"(或"環")。
並且,若路徑中各頂點都不重複,此路徑又被稱為"簡單路徑";同樣,若迴路中的頂點互不重複,此迴路被稱為"簡單迴路"(或簡單環)。
上圖中,從 V1 存在一條路徑還可以回到 V1,此路徑為 {V1,V3,V4,V1},這是一個迴路(環),而且還是一個簡單迴路(簡單環)。子圖
指的是由圖中一部分頂點和邊構成的圖,稱為原圖的子圖。
連通圖
無向圖中,如果任意兩個頂點之間都能夠連通,則稱此無向圖為連通圖。例如,下圖中的無向圖就是一個連通圖,因為此圖中任意兩頂點之間都是連通的。
若無向圖不是連通圖,但圖中儲存某個子圖符合連通圖的性質,則稱該子圖為連通分量。需要注意的是,連通分量的提出是以"整個無向圖不是連通圖"為前提的,因為如果無向圖是連通圖,則其無法分解出多個最大連通子圖,因為圖中所有的頂點之間都是連通的。
強連通圖
有向圖中,若任意兩個頂點 Vi 和 Vj,滿足從 Vi 到 Vj 以及從 Vj 到 Vi 都連通,也就是都含有至少一條通路,則稱此有向圖為強連通圖。
與此同時,若有向圖本身不是強連通圖,但其包含的最大連通子圖具有強連通圖的性質,則稱該子圖為強連通分量。
圖的儲存
鄰接矩陣
所謂鄰接矩陣(Adjacency Matrix)的儲存結構,就是用一維陣列儲存圖中頂點的資訊,用矩陣表示圖中各頂點之間的鄰接關係。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXVEX 100 /* 最大頂點數,應由使用者定義 */
#define INFINITYC 0
typedef int Status; /* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef char VertexType; /* 頂點型別應由使用者定義 */
typedef int EdgeType; /* 邊上的權值型別應由使用者定義 */
typedef struct
{
VertexType vexs[MAXVEX]; /* 頂點表 */
EdgeType arc[MAXVEX][MAXVEX];/* 鄰接矩陣,可看作邊表 */
int numNodes,numEdges; /* 圖中當前的頂點數和邊數 */
}MGraph;
void CreateMGraph(MGraph *G){
int i,j,k,w;
printf("輸入頂點數和邊數:\n");
//1. 輸入頂點數/邊數
scanf("%d,%d",&G->numNodes,&G->numEdges);
printf("頂點數:%d,邊數:%d\n",G->numNodes,G->numEdges);
//2.輸入頂點資訊/頂點表
for(i = 0; i<= G->numNodes;i++)
scanf("%c",&G->vexs[i]);
//3.初始化鄰接矩陣
for(i = 0; i < G->numNodes;i++)
for(j = 0; j < G->numNodes;j++)
G->arc[i][j] = INFINITYC;
//4.輸入邊表資訊
for(k = 0; k < G->numEdges;k++){
printf("輸入邊(vi,vj)上的下標i,下標j,權w\n");
scanf("%d,%d,&i,&j,&w);
G->arc[i][j] = w;
//如果無向圖,矩陣對稱;
G->arc[j][i] = G->arc[i][j];
}
/*5.列印鄰接矩陣*/
for (int i = 0; i < G->numNodes; i++) {
printf("\n");
for (int j = 0; j < G->numNodes; j++) {
printf("%d ",G->arc[i][j]);
}
}
printf("\n");
}
int main(void)
{
printf("鄰接矩陣實現圖的儲存\n");
/*圖的儲存-鄰接矩陣*/
MGraph G;
CreateMGraph(&G);
return 0;
}
複製程式碼
鄰接表
鄰接表是一種順序儲存與鏈式儲存結合的儲存方法。鄰接表表示法類似於樹的孩子連結串列表示法。就是對於圖G中的每個頂點vi,將所有鄰接於vi的頂點vj鏈成一個單連結串列,這個單連結串列就稱為頂點vi的鄰接表,再將所有頂點的鄰接表表頭放到陣列中,就構成了圖的鄰接表。
#define M 100
#define true 1
#define false 0
typedef char Element;
typedef int BOOL;
//鄰接表的節點
typedef struct Node{
int adj_vex_index; //弧頭的下標,也就是被指向的下標
Element data; //權重值
struct Node * next; //邊指標
}EdgeNode;
//頂點節點表
typedef struct vNode{
Element data; //頂點的權值
EdgeNode * firstedge; //頂點下一個是誰?
}VertexNode,Adjlist[M];
//總圖的一些資訊
typedef struct Graph{
Adjlist adjlist; //頂點表
int arc_num; //邊的個數
int node_num; //節點個數
BOOL is_directed; //是不是有向圖
}Graph,*GraphLink;
void creatGraph(GraphLink *g){
int i,k;
EdgeNode *p;
//1. 頂點,邊,是否有向
printf("輸入頂點數目,邊數和有向?:\n");
scanf("%d %d %d",&(*g)->node_num,&(*g)->arc_num,&(*g)->is_directed);
//2.頂點表
printf("輸入頂點資訊:\n");
for (i = 0; i < (*g)->node_num; i++) {
getchar();
scanf("%c",&(*g)->adjlist[i].data);
(*g)->adjlist[i].firstedge = NULL;
}
//3.
printf("輸入邊資訊:\n");
for (k = 0; k < (*g)->arc_num; k++){
getchar();
scanf("%d %d",&j);
//①新建一個節點
p = (EdgeNode *)malloc(sizeof(EdgeNode));
//②弧頭的下標
p->adj_vex_index = j;
//③頭插法插進去,插的時候要找到弧尾,那就是頂點陣列的下標i
p->next = (*g)->adjlist[i].firstedge;
//④將頂點陣列[i].firstedge 設定為p
(*g)->adjlist[i].firstedge = p;
//j->i
if(!(*g)->is_directed)
{
// j -----> i
//①新建一個節點
p = (EdgeNode *)malloc(sizeof(EdgeNode));
//②弧頭的下標i
p->adj_vex_index = i;
//③頭插法插進去,插的時候要找到弧尾,那就是頂點陣列的下標i
p->next = (*g)->adjlist[j].firstedge;
//④將頂點陣列[i].firstedge 設定為p
(*g)->adjlist[j].firstedge = p;
}
}
}
void putGraph(GraphLink g){
int i;
printf("鄰接表中儲存資訊:\n");
//遍歷一遍頂點座標,每個再進去走一次
for (i = 0; i < g->node_num; i++) {
EdgeNode * p = g->adjlist[i].firstedge;
while (p) {
printf("%c->%c ",g->adjlist[i].data,g->adjlist[p->adj_vex_index].data);
p = p->next;
}
printf("\n");
}
}
複製程式碼
圖的遍歷
深度優先遍歷
所謂深度優先遍歷,是從圖中的一個頂點出發,每次遍歷當前訪問頂點的臨界點,一直到訪問的頂點沒有未被訪問過的臨界點為止。然後採用依次回退的方式,檢視來的路上每一個頂點是否有其它未被訪問的臨界點。訪問完成後,判斷圖中的頂點是否已經全部遍歷完成,如果沒有,以未訪問的頂點為起始點,重複上述過程。
例如上圖是一個無向圖,採用深度優先演演算法遍歷這個圖的過程為:- 首先任意找一個未被遍歷過的頂點,例如從 V1 開始,由於 V1 率先訪問過了,所以,需要標記 V1 的狀態為訪問過;
- 然後遍歷 V1 的鄰接點,例如訪問 V2 ,並做標記,然後訪問 V2 的鄰接點,例如 V4 (做標記),然後 V8 ,然後 V5 ;
- 當繼續遍歷 V5 的鄰接點時,根據之前做的標記顯示,所有鄰接點都被訪問過了。此時,從 V5 回退到 V8 ,看 V8 是否有未被訪問過的鄰接點,如果沒有,繼續回退到 V4 , V2 , V1 ;
- 通過檢視 V1 ,找到一個未被訪問過的頂點 V3 ,繼續遍歷,然後訪問 V3 鄰接點 V6 ,然後 V7 ;
- 由於 V7 沒有未被訪問的鄰接點,所有回退到 V6 ,繼續回退至 V3 ,最後到達 V1 ,發現沒有未被訪問的;
- 最後一步需要判斷是否所有頂點都被訪問,如果還有沒被訪問的,以未被訪問的頂點為第一個頂點,繼續依照上邊的方式進行遍歷。
通過深度優先遍歷獲得的頂點的遍歷次序為: V1 -> V2 -> V4 -> V8 -> V5 -> V3 -> V6 -> V7
深度優先遍歷是一個不斷回溯的過程。
程式碼實現
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef int Boolean; /* Boolean是布林型別,其值是TRUE或FALSE */
typedef char VertexType; /* 頂點型別應由使用者定義 */
typedef int EdgeType; /* 邊上的權值型別應由使用者定義 */
#define MAXSIZE 9 /* 儲存空間初始分配量 */
#define MAXEDGE 15
#define MAXVEX 9
#define INFINITYC 65535
typedef struct
{
VertexType vexs[MAXVEX]; /* 頂點表 */
EdgeType arc[MAXVEX][MAXVEX];/* 鄰接矩陣,可看作邊表 */
int numVertexes,numEdges; /* 圖中當前的頂點數和邊數 */
}MGraph;
/*4.1 構建一個鄰接矩陣*/
void CreateMGraph(MGraph *G)
{
int i,j;
//1. 確定圖的頂點數以及邊數
G->numEdges=15;
G->numVertexes=9;
/*2.讀入頂點資訊,建立頂點表 */
G->vexs[0]='A';
G->vexs[1]='B';
G->vexs[2]='C';
G->vexs[3]='D';
G->vexs[4]='E';
G->vexs[5]='F';
G->vexs[6]='G';
G->vexs[7]='H';
G->vexs[8]='I';
/*3. 初始化圖中的邊表*/
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
/*4.將圖中的連線資訊輸入到邊表中*/
G->arc[0][1]=1;
G->arc[0][5]=1;
G->arc[1][2]=1;
G->arc[1][8]=1;
G->arc[1][6]=1;
G->arc[2][3]=1;
G->arc[2][8]=1;
G->arc[3][4]=1;
G->arc[3][7]=1;
G->arc[3][6]=1;
G->arc[3][8]=1;
G->arc[4][5]=1;
G->arc[4][7]=1;
G->arc[5][6]=1;
G->arc[6][7]=1;
/*5.無向圖是對稱矩陣.構成對稱*/
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/*4.2 DFS遍歷*/
Boolean visited[MAXVEX]; /* 訪問標誌的陣列 */
//1. 標識頂點是否被標記過;
//2. 選擇從某一個頂點開始(注意:非連通圖的情況)
//3. 進入遞迴,列印i點資訊,標識; 邊表
//4. [i][j] 是否等於1,沒有變遍歷過visted
void DFS(MGraph G,int i){
//1.
visited[i] = TRUE;
printf("%c",G.vexs[i]);
//2.0~numVertexes
for(int j = 0; j < G.numVertexes;j++){
if(G.arc[i][j] == 1 && !visited[j])
DFS(G,j);
}
}
void DFSTravese(MGraph G){
//1.初始化
for(int i=0;i<G.numVertexes;i++){
visited[i] = FALSE;
}
//2.某一個頂點
for(int i = 0;i<G.numVertexes;i++){
if(!visited[i]){
DFS(G,i);
}
}
}
int main(int argc,const char * argv[]) {
// insert code here...
printf("鄰接矩陣的深度優先遍歷!\n");
MGraph G;
CreateMGraph(&G);
DFSTravese(G);
printf("\n");
return 0;
}
複製程式碼
廣度優先遍歷
廣度優先搜尋類似於樹的層次遍歷。從圖中的某一頂點出發,遍歷每一個頂點時,依次遍歷其所有的鄰接點,然後再從這些鄰接點出發,同樣依次訪問它們的鄰接點。按照此過程,直到圖中所有被訪問過的頂點的鄰接點都被訪問到。
最後還需要做的操作就是檢檢視中是否存在尚未被訪問的頂點,若有,則以該頂點為起始點,重複上述遍歷的過程。
還拿上面的無向圖為例,假設 V1 作為起始點,遍歷其所有的鄰接點 V2 和 V3 ,以 V2 為起始點,訪問鄰接點 V4 和 V5 ,以 V3 為起始點,訪問鄰接點 V6 、 V7 ,以 V4 為起始點訪問 V8 ,以 V5 為起始點,由於 V5 所有的起始點已經全部被訪問,所有直接略過, V6 和 V7 也是如此。 以 V1 為起始點的遍歷過程結束後,判斷圖中是否還有未被訪問的點,由於圖 1 中沒有了,所以整個圖遍歷結束。
遍歷頂點的順序為:V1 -> V2 -> v3 -> V4 -> V5 -> V6 -> V7 -> V8
程式碼實現
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函式的型別,j;
//1. 確定圖的頂點數以及邊數
G->numEdges=15;
G->numVertexes=9;
/*2.讀入頂點資訊,建立頂點表 */
G->vexs[0]='A';
G->vexs[1]='B';
G->vexs[2]='C';
G->vexs[3]='D';
G->vexs[4]='E';
G->vexs[5]='F';
G->vexs[6]='G';
G->vexs[7]='H';
G->vexs[8]='I';
/*3. 初始化圖中的邊表*/
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
/*4.將圖中的連線資訊輸入到邊表中*/
G->arc[0][1]=1;
G->arc[0][5]=1;
G->arc[1][2]=1;
G->arc[1][8]=1;
G->arc[1][6]=1;
G->arc[2][3]=1;
G->arc[2][8]=1;
G->arc[3][4]=1;
G->arc[3][7]=1;
G->arc[3][6]=1;
G->arc[3][8]=1;
G->arc[4][5]=1;
G->arc[4][7]=1;
G->arc[5][6]=1;
G->arc[6][7]=1;
/*5.無向圖是對稱矩陣.構成對稱*/
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
/*
4.2 ***需要用到的佇列結構與相關功能函式***
*/
/* 迴圈佇列的順序儲存結構 */
typedef struct
{
int data[MAXSIZE];
int front; /* 頭指標 */
int rear; /* 尾指標,若佇列不空,指向佇列尾元素的下一個位置 */
}Queue;
/* 初始化一個空佇列Q */
Status InitQueue(Queue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
/* 若佇列Q為空佇列,則返回TRUE,否則返回FALSE */
Status QueueEmpty(Queue Q)
{
if(Q.front==Q.rear) /* 佇列空的標誌 */
return TRUE;
else
return FALSE;
}
/* 若佇列未滿,則插入元素e為Q新的隊尾元素 */
Status EnQueue(Queue *Q,int e)
{
if ((Q->rear+1)%MAXSIZE == Q->front) /* 佇列滿的判斷 */
return ERROR;
Q->data[Q->rear]=e; /* 將元素e賦值給隊尾 */
Q->rear=(Q->rear+1)%MAXSIZE;/* rear指標向後移一位置, */
/* 若到最後則轉到陣列頭部 */
return OK;
}
/* 若佇列不空,則刪除Q中隊頭元素,用e返回其值 */
Status DeQueue(Queue *Q,int *e)
{
if (Q->front == Q->rear) /* 佇列空的判斷 */
return ERROR;
*e=Q->data[Q->front]; /* 將隊頭元素賦值給e */
Q->front=(Q->front+1)%MAXSIZE; /* front指標向後移一位置, */
/* 若到最後則轉到陣列頭部 */
return OK;
}
/******** Queue End **************/
/*4.3 鄰接矩陣廣度優先遍歷-程式碼實現*/
Boolean visited[MAXVEX]; /* 訪問標誌的陣列 */
void BFSTraverse(MGraph G){
int temp = 0;
//1.
Queue Q;
InitQueue(&Q);
//2.將訪問標誌陣列全部置為"未訪問狀態FALSE"
for (int i = 0 ; i < G.numVertexes; i++) {
visited[i] = FALSE;
}
//3.對遍歷鄰接表中的每一個頂點(對於連通圖只會執行1次,這個迴圈是針對非連通圖)
for (int i = 0 ; i < G.numVertexes; i++) {
if(!visited[i]){
visited[i] = TRUE;
printf("%c ",G.vexs[i]);
//4. 入隊
EnQueue(&Q,i);
while (!QueueEmpty(Q)) {
//出隊
DeQueue(&Q,&i);
for (int j = 0; j < G.numVertexes; j++) {
if(G.arc[i][j] == 1 && !visited[j])
{ visited[j] = TRUE;
printf("%c ",G.vexs[j]);
EnQueue(&Q,j);
}
}
}
}
}
}
int main(int argc,const char * argv[]) {
// insert code here...
printf("鄰接矩陣廣度優先遍歷!\n");
MGraph G;
CreateMGraph(&G);
BFSTraverse(G);
printf("\n");
return 0;
}
複製程式碼