数据结构中,图是很重要的一部分,比线性表和树型结构更加复杂。线性表中数据有很明显的前驱和后继关系;树型结构中数据有很明显的层次关系,父层和子层,千层饼一样;而图所表示的数据,任意两个之间都可以有关系。首先介绍一些图中的专用名词,下图表示一种常见的图。

                                                           数据结构图小结

       图中数据元素叫做顶点(vertex),如上图中的A、B、C等,顶点之间的连线叫做(arc),如<a,b>可以表示从A点到B点的弧,如果是A指向B的弧,那么A为弧尾初始点,B为弧头终端点,这种有弧方向的图是有向图(digraph),若两个顶点之间只是用有无连接关系表示的话,就可以用来表示,此时的图是无向图(undigraph)。假若弧或边上带有权值信息,那么图就称为网,所以图也可以分为无向图、有向图、无向网和有向网四类。对于无向图,相邻的两个顶点互为邻接点,顶点A的是指和顶点A相关联的边的数目。对于有向图,度分为出度入度,A的出度就是以A为弧尾的弧的个数,A的入度就是以A为弧头的弧的个数,常见的图中名词有以上这些。

       图的表示法有数组法邻接表法十字链表法。当然这些只是常见的,具体的图的存储结构还要跟实际情况结合起来,可以组合搭配使用,不能拘泥于一种方法,领悟其精华,学会变通即可。下面具体讲一下他们的表示方法和创建图的过程。

图的数组表示法

       图的存储结构用数组表示的话,C语言版本的结构如下所示:

[cpp] view plain copy
  1. #define MAX_VERTEX_NUM 10           //最大顶点vertex数  
  2. #define MAX_NAME 5                  //顶点向量字符最长数+1  
  3. #define MAX_INIT 65535              //对于网,将值设为无限大表示没有弧存在  
  4. typedef int VRType;                 //表示顶点的关系类型,对于无权图,有0和1,对网可以表示其权值  
  5. typedef char VertexType[MAX_NAME];  //顶点名字信息  
  6. typedef enum{DG, DN, UDG, UDN} GraphKind;//图的类型  
  7. typedef struct ArcCell{  
  8.     VRType adj;  
  9.     //这里可以添加一些其他信息,用来表示弧相关其他信息  
  10. }ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];  
  11. typedef struct {  
  12.     VertexType vexs[MAX_VERTEX_NUM];//顶点向量,就是顶点的名字,可以用字符串表示  
  13.     AdjMatrix arcs;                 //邻接矩阵  
  14.     int vexnum,arcnum;              //顶点数和弧数  
  15.     GraphKind kind;                 //图的类型  
  16. }Graph;  

       上面都详细的描述了定义的结构体中各个的含义,其实用数组表示图最主要的就是那个矩阵,矩阵中存放着“权值”的信息,其他的就是顶点个数、弧个数、顶点名字等,用一个结构体也是可以的。创建图的话就要根据图的类型进行操作了,我主要创建了无向图和有向网,这两个可以的话剩下两个应该都不难。无向图的表示也最为简单,创建过程如下程序所示:

[cpp] view plain copy
  1. void CreateUDG(Graph *G)    
  2. {    
  3.     int i, j, k;    
  4.     VertexType  va, vb;                 //临时变量,存储顶点A和B  
  5.           
  6.     printf("Input the number of the vertex and arc:\n");  
  7.     scanf("%d %d",&G->vexnum, &G->arcnum);  
  8.     printf("input the name of the vertex:\n");  
  9.     for(i=0; i<G->vexnum; i++)  
  10.         scanf("%s",G->vexs[i]);  
  11.     for (i = 0; i < G->vexnum; ++i)      // 初始化邻接矩阵    
  12.         for (j = 0; j < G->vexnum; ++j)    
  13.         {    
  14.             G->arcs[i][j].adj  = 0;      // 无向图,所以都初始值为0    
  15.         }    
  16.     //根据顶点向量的信息判断两点之间有无连接  
  17.     for (k = 0; k < G->arcnum; ++k)    
  18.     {    
  19.     printf("\nInput the first vertex:\n");  
  20.         scanf("%s",va);  
  21.         i = Locate(G, va);               //找出va在图中的位置    
  22.         printf("\nInput the last vertex:\n");  
  23.     scanf("%s",vb);  
  24.     j = Locate(G, vb);    
  25.         G->arcs[i][j].adj = G->arcs[j][i].adj = 1;    // 无向图 ,两个顶点是一样的  
  26.     }    
  27.     G->kind = UDG;    
  28. }   

       上述程序就是创建无向图的过程,这个只是简单的示范,所以比较简单。对于有向网,稍微复杂一点,就是加了权值,定了弧的方向,实现方式如下所示:

[cpp] view plain copy
  1. void CreateDN(Graph *G){  
  2.     int i,j,k;  
  3.     VertexType va,vb;  
  4.     printf("Please input the vertexnum and the arcnum:\n");  
  5.     scanf("%d %d",&G->vexnum,&G->arcnum);  
  6.     for(i=0;i<G->vexnum;i++){  
  7.         for(j=0;j<G->vexnum;j++){  
  8.             G->arcs[i][j].adj = MAX_INIT;            //初始化为大数,表示达不到或者说没有A到B顶点的弧  
  9.         }  
  10.     }  
  11.     printf("\nInput the name of the vertex:\n");  
  12.     for(i=0;i<G->vexnum;i++){  
  13.         scanf("%s",G->vexs[i]);  
  14.     }  
  15.     for(k=0;k<G->arcnum;k++){  
  16.         printf("\nInput the first vertex:\n");  
  17.         scanf("%s",va);  
  18.         i = Locate(G, va);  
  19.         printf("\nInput the second vertex:\n");  
  20.         scanf("%s",vb);  
  21.         j = Locate(G, vb);  
  22.         printf("\nInput the weight of the vector:\n");  
  23.         VRType m;  
  24.         scanf("%d",&m);  
  25.         G->arcs[i][j].adj = m;  
  26.     }  
  27.     G->kind = DN;  
  28. }  

       需要指出的是,这两个创建过程都有用到locate函数,该函数是为了实现查找到顶点在图中的位置。具体实现如下:

[cpp] view plain copy
  1. int Locate(Graph *g, VertexType v){  
  2.     int i;  
  3.     for(i=0; i<g->vexnum; i++){  
  4.         if(strcmp(v, g->vexs[i]) == 0)  
  5.             return i;  
  6.     }  
  7.     return -1;  
  8. }  

        具体的实现方法,可以参见我的源码:https://github.com/clarkzhang56/useful-data-structure/blob/master/Graph/graphwitharray.c

图的邻接表表示法

       邻接表是图的一种链式存储结构。邻接表中,对每个顶点建一个单链表,单链表中的结点表示依附于顶点的边或弧。每个结点包括三部分,该顶点的位置、所指向的下一个结点和弧相关的信息,比如权值。每个链表都有一个表头结点,表头结点包含指向链表的第一个结点的链域和存储顶点名的数据域,具体的结构如下所示:

[cpp] view plain copy
  1. #define MAX_VEXTEX 10  
  2. #define MAX_LEN 5  
  3. typedef enum{UDG,UDN,DG,DN} GraphKind;  
  4. typedef char VertexType[MAX_LEN];     
  5. typedef struct ArcNode{               
  6.     int adjvex;             //弧指向的顶点的位置信息  
  7.     struct ArcNode *nextArc;        //下一个结点  
  8.     int weight;             //权值,当然也可以是其他信息  
  9. }ArcNode;  
  10. typedef struct VNode{               //表头结点  
  11.     VertexType data;            //数据域,存储顶点名称  
  12.     ArcNode *firstArc;          //链域,指向第一个结点  
  13. }VNode,AdjList[MAX_VEXTEX];  
  14. typedef struct Graph{  
  15.     int vextexnum,arcnum;  
  16.     AdjList vertices;  
  17.     GraphKind kind;  
  18. }Graph;  

       我用邻接表创建了有向网,有向网的实现,最困难的地方在于增加结点信息的时候,比如添加完firstarc,只要让其指向一个新增加的内存即可,但是如果其后面还有结点呢?我最开始的想法是使用双指针,发现效果还不错,这样可以修改firstarc指向的后一个结点的内存信息,即修改其值等,我发现效果还不错。我写的错误程序如下所示(可以在看完整个程序后再看这个也行):

[cpp] view plain copy
  1. ArcNode **tmpnextarc = &G->vertices[i].firstArc->nextArc;  
  2. while(*tmpnextarc != NULL){  
  3. *tmpnextarc = (*tmpnextarc)->nextArc;  
  4. }  
  5. *tmpnextarc = tmp;  
  6. (*tmpnextarc)->nextArc = NULL;  

       可是,当我尝试再增加第三个结点的时候,问题出现了,我在print图的时候只能显示第一个和第三个结点的信息,不会显示第二个,我就纳闷了,怎么会没有呢,第二个被我吃了?想了很久,终于想明白了。当firstarc新增一个结点的时候,双指针可以指向新结点的地址,同时修改其地址的内容,但是再增加一个结点的时候,双指针指向的内存地址没有变化,即使让它指向另一个地址,其也不能修改那一个地址的内容,因为指向另一个地址的时候,它就不再是双指针了。修改的地址还是原来的地址,所以这种方法行不通。后来我又看了一下书本(基础都在书本啊),发现可以用指针的nextarc指向firstarc,然后赋值给firstarc就行了。这种方法是将结点加在了firstarc的头部,而不是尾部,比较厉害。具体的创建有向网的程序如下所示:

[cpp] view plain copy
  1. void CreateDN(Graph *G){  
  2.     printf("Create the Digraph Net:\n");  
  3.     int i,j,k;  
  4.     VertexType vf,vl;  
  5.     printf("Please input the vextexnum and the arcnum:\n");  
  6.     scanf("%d %d",&G->vextexnum,&G->arcnum);  
  7.     printf("Please input the name of the vextexnum:\n");      
  8.     for(i=0;i<G->vextexnum;i++){              //初始化  
  9.         scanf("%s",G->vertices[i].data);  
  10.         G->vertices[i].firstArc = NULL;  
  11.     }  
  12.     for(k=0;k<G->arcnum;k++){  
  13.         printf("Please input the first vertex:\n");  
  14.         scanf("%s",vf);  
  15.         printf("Please input the last vertex:\n");  
  16.         scanf("%s",vl);  
  17.         i = Locate(G,vf);                       //寻找顶点的位置,和数组表示图那里一样  
  18.         j = Locate(G,vl);  
  19.         if(i != -1 && j != -1){  
  20.             ArcNode *tmp = (ArcNode*) malloc (sizeof(ArcNode));  
  21.             tmp->adjvex = j;           
  22.             printf("Please input the weight:\n");  
  23.             scanf("%d",&tmp->weight);  
  24.             /*  以下实现方法比较方便,易懂还方便    */  
  25.             tmp->nextArc = G->vertices[i].firstArc;  
  26.             G->vertices[i].firstArc = tmp;  
  27.         }  
  28.     }  
  29.     G->kind = DN;  
  30. }  

       有向网就创建成功了。另外三个都比这个要简单,我就不写了。具体文档可以参考如下链接:https://github.com/clarkzhang56/useful-data-structure/blob/master/Graph/graphwithadjacencylist.c

图的十字链表表示法

       虽然数组和邻接表都可以作为图的存储结构,但是它们都有一些弊端:数组表示法要浪费很多的空间;邻接表可以容易查找到某一顶点的出度,但是很难找到该顶点的入度。为了解决这个问题,可以使用十字链表。十字链表其实就是邻接表的升级版。它只针对有向图(因为无向图的话没有入度-_-!),每一条弧都有一个结点,对应每一个顶点也有一个结点,弧结点包括:弧头、弧尾、同一弧头的弧、同一弧尾的弧和弧信息(比如权值),顶点结点包括:顶点名字、以该顶点为弧头的第一个结点和以该顶点为弧尾的第一个结点。具体结构定义如下所示:

[cpp] view plain copy
  1. #define MAX_NUM 5  
  2. #define MAX_VEX_NUM 20  
  3. typedef char VexType[MAX_NUM];   
  4. typedef struct AcrBox{            
  5.     int tailvex,headvex;        //弧尾和弧头的位置  
  6.     struct AcrBox *hlink;       //相同弧头的下一结点  
  7.     struct AcrBox *tlink;  
  8.     int weight;  
  9. }ArcBox;  
  10. typedef struct VexNode{           
  11.     VexType data;  
  12.     ArcBox *firstin;            //指向以该顶点为弧头的第一个结点  
  13.     ArcBox *firstout;  
  14. }VexNode;  
  15. typedef struct {  
  16.     VexNode xlist[MAX_VEX_NUM];  
  17.     int vexnum, arcnum;  
  18. }GraphDN;  

       这里我只以有向网为例子,因为有向网的稍微复杂一点,创建有向网的难点也在于增加结点的部分,所以这里也采用了和邻接表法相同的方法,即加在头部而不是尾部的方法。具体创建有向网的过程如下:

[cpp] view plain copy
  1. void CreateDNgraph(GraphDN *G){  
  2.     int i,j,k;  
  3.     VexType tail,head;  
  4.     printf("Creating the Diagraph Net with orthogonal list......\n");  
  5.     printf("Input the num of the vextex and arc:\n");  
  6.     scanf("%d %d",&G->vexnum, &G->arcnum);  
  7.     printf("Input the vextex name:\n");  
  8.     for(i=0; i<G->vexnum; i++){               //初始化  
  9.         scanf("%s",G->xlist[i].data);  
  10.         G->xlist[i].firstin = G->xlist[i].firstout = NULL;  
  11.     }  
  12.     for(k=0; k<G->arcnum; k++){  
  13.         printf("Input the tailvex name:\n");  
  14.         scanf("%s",tail);  
  15.         printf("Input the headvex name:\n");  
  16.         scanf("%s",head);  
  17.         i = Locate(G, tail);  
  18.         j = Locate(G, head);  
  19.         if(i != -1 && j != -1){  
  20.             ArcBox *arcbox = (ArcBox *)malloc(sizeof(ArcBox));  
  21.             arcbox->tailvex = i;  
  22.             arcbox->headvex = j;  
  23.             printf("Input the weight:\n");  
  24.             scanf("%d",&arcbox->weight);  
  25.             /*  难点和精华,搞清楚头和尾很重要 */  
  26.             arcbox->tlink = G->xlist[i].firstout;  
  27.             G->xlist[i].firstout = arcbox;  
  28.             arcbox->hlink = G->xlist[j].firstin;  
  29.             G->xlist[j].firstin = arcbox;  
  30.         }else{  
  31.             printf("Not the vextex name.\n");  
  32.             --k;  
  33.         }  
  34.     }  
  35. }  

       具体可以参考右边链接:https://github.com/clarkzhang56/useful-data-structure/blob/master/Graph/graphwithorthogonallist.c

图的遍历

       图的遍历有深度优先搜索(Deepth first search)和广度优先搜索(Breadth first search),二者效率是一样的,时间复杂度为O(n+e)。

深度优先搜索

       顾名思义,就是一步一步先搜“深”的,再回过头来搜“浅”的,这就要用到迭代了。搜索就是遍历整个图,把所有顶点遍历完,这就需要先定义一个数组,用来表示该顶点是否访问过。如果没访问过,就进行搜索,可以通过打印顶点名字表示搜索了,搜索后还要把该顶点表示为搜索过了。数组表示法有向网的深度优先搜索如下所示:

[cpp] view plain copy
  1. #define true 1  
  2. #define false 0  
  3. typedef int bool;  
  4. bool visited[MAX_VERTEX_NUM];  
  5. void DFS(Graph *g, int v){  
  6.     int i;  
  7.     visited[v] = true;  
  8.     printf("%s  ",g->vexs[v]);  
  9.     for(i=0; i<g->vexnum; i++){  
  10.         if(g->arcs[v][i].adj != 65535 && visited[i] == false)    //为最大数表示没有弧存在  
  11.             DFS(g, i);  
  12.     }  
  13. }  
  14. void DFSgraph(Graph *G){  
  15.     int i;  
  16.     for(i=0; i<G->vexnum; i++){  
  17.         visited[i] = false;  
  18.     }  
  19.     for(i=0; i<G->vexnum; i++){  
  20.         if(!visited[i]) DFS(G, i);  
  21.     }  
  22. }  

       需要说明的是,在C语言中木有bool,true和false,所以都要自己定义。图的另外两种表示法的深度优先搜索和这个都类似。

广度优先搜索

       这种搜索方式就和树的逐层遍历一样,先搜索A指向的所有顶点,然后搜索A指向的第一个顶点所指向的所有顶点,直到遍历完图,如果还有未搜索的,就再搜索它,和深度优先搜索的不同之处仅仅在于顶点的访问顺序不同。

相关文章: