1. 程式人生 > Android開發 >資料結構與演演算法(十一):圖的儲存與遍歷

資料結構與演演算法(十一):圖的儲存與遍歷

圖的定義

圖(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");
    }
}

複製程式碼

圖的遍歷

深度優先遍歷

所謂深度優先遍歷,是從圖中的一個頂點出發,每次遍歷當前訪問頂點的臨界點,一直到訪問的頂點沒有未被訪問過的臨界點為止。然後採用依次回退的方式,檢視來的路上每一個頂點是否有其它未被訪問的臨界點。訪問完成後,判斷圖中的頂點是否已經全部遍歷完成,如果沒有,以未訪問的頂點為起始點,重複上述過程。

例如上圖是一個無向圖,採用深度優先演演算法遍歷這個圖的過程為:

  1. 首先任意找一個未被遍歷過的頂點,例如從 V1 開始,由於 V1 率先訪問過了,所以,需要標記 V1 的狀態為訪問過;
  2. 然後遍歷 V1 的鄰接點,例如訪問 V2 ,並做標記,然後訪問 V2 的鄰接點,例如 V4 (做標記),然後 V8 ,然後 V5 ;
  3. 當繼續遍歷 V5 的鄰接點時,根據之前做的標記顯示,所有鄰接點都被訪問過了。此時,從 V5 回退到 V8 ,看 V8 是否有未被訪問過的鄰接點,如果沒有,繼續回退到 V4 , V2 , V1 ;
  4. 通過檢視 V1 ,找到一個未被訪問過的頂點 V3 ,繼續遍歷,然後訪問 V3 鄰接點 V6 ,然後 V7 ;
  5. 由於 V7 沒有未被訪問的鄰接點,所有回退到 V6 ,繼續回退至 V3 ,最後到達 V1 ,發現沒有未被訪問的;
  6. 最後一步需要判斷是否所有頂點都被訪問,如果還有沒被訪問的,以未被訪問的頂點為第一個頂點,繼續依照上邊的方式進行遍歷。

通過深度優先遍歷獲得的頂點的遍歷次序為: 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;
}

複製程式碼