数据结构看书笔记(九)--图的存储结构及遍历

来源:互联网 发布:苹果官网软件 编辑:程序博客网 时间:2024/06/16 03:53
图的抽象数据类型:
ADT 图(Graph)Data顶点的有穷非空集合和边的集合OperationCreateGraph(*G,V,VR):按照顶点集合V和边弧集VR的定义构造图G。DestroyGraph(*G):图G存在则销毁。LocateVex(G,u):若图G中存在顶点u,则返回图中的位置。GetVex(G,v):返回图G中顶点v的值。PutVex(G,v,value):将图G中顶点v赋值value。FirstAdjvex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空。NextAdjVex(G,v,*w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回“空”。InsertVex(*G,v):在图G中增添新顶点v.DeleteVex(*G,v):删除图G中顶点v及其相关的弧。InsertArc(*G,v,w):在图G中增添弧<v,w>,若G是无向图,还需要增添对称弧<w,v>。DeleteArc(*G,v,w):在图G中删除弧<v,w>,若G是无向图,则还删除对称弧<w,v>。DESTraverse(G):对图G中进行深度优先遍历,在遍历过程中对每个顶点调用。HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程中对每个顶点调用。endADT



图的存储结构:
由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素的在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。而多重链表的方式,即以一个数据域和多个指针域组成的结点表示图中的一个顶点,尽管可以实现图的结构,单如果各个顶点的度数相差太大,按度数最大的顶点来设计结点会造成很大浪费,按每个顶点的度数来设计则操作又会变得很困难。因此,实现其物理存储是一个很大的难题。
但是,尽管这个问题很难解决,而前人还是给出了以下五种存储方式。

邻接矩阵:
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

arc[i][j] = {1,若(vi,vj)∈E或<vi,vj>∈E;0,则反之

插图例子(无向图):

我们可以设置两个数组,顶点数组为vertex[4] = {v0,v1,v2,v3},边数组arc[4][4]为上图右边所示的这样的一个矩阵。
因为不存在顶点到自身的边,所以对角线的值全为0;而arc[i][j]=1时,表示顶点vi到vj的边存在。
另外,由于上图可见是一个无向图,无向图的邻接矩阵是一个对称矩阵。对称矩阵的概念:aij=aji,(0<=i,j<=n).

有了该矩阵,我们可以很清楚的知道图中的信息:
1.要判定任意两顶点是否有边无边就非常容易了。
2.要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。
3.求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点。

插图例子(有向图):


顶点数组为vertex[4] = {v0,v1,v2,v3},弧数组arc[4][4]为上图右图这样的一个矩阵。主对角线上数值依然为0。但因为是有向图,故不是对称矩阵,其余类似上面无向图的定义。

有向图讲究入度出度,顶点vi的入度是第vi列各数之和。顶点vi的出度为第vi行各数之和。
判断顶点vi到vj是否存在弧,只需要查找矩阵中arc[i][j]是否为1即可。
要求vi的邻接点就是把矩阵第vi行元素扫描一遍,查找v[i][j]为1的顶点。


关于网:每条边上带有权的图叫做网。如何存储这些权值呢?
>>>设图G网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

插入图:

arc[i][j]={Wij,若(vi,vj)∈E或<vi,vj>∈E;0,若i=j; ∞,反之。

这里Wij表示(vi,vj)或<vi,vj>上的权值。∞表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。
疑问:∞为何不用0呢?>>>原因在于Wij大多数情况下是正值,但个别时候可能是0,甚至有可能是负值。因此必须用一个不可能的值来代表不存在。
下面给出一个有向网图,右图为一个邻接矩阵:


邻接矩阵的创建:
图的邻接矩阵存储结构的代码:
typedef char VertexType;//顶点类型,由用户定义typedef int EdgeType;//边上的权值类型,由用户定义#define MAXVEX 100//最大顶点数,应由用户定义#define INFINITY 65535//用65535来代表∞
typedef struct{VertexType vexs[MAXVEX];//顶点表EdgeType arc[MAXVEX][MAXVEX];//邻接矩阵,可看作边表int numVertexes,numEdges;//图中当前的顶点数和边数}MGraph;

有了以上的结构定义,那么则可以构造一个图,其实就是给顶点表和边表输入数据的过程。
以下给出无向网图的创建代码:
void CreateMGraph(MGraph *G){int i,j,k,w;printf("输入顶点数和边数:\n");scanf("%d,%d",&G->numVertexes,&G->numEdges);//输入顶点数和边数for(i=0;i<G->numVertexes;i++)scanf(&G->vexs[i]);for(i=0;i<G->numVertexes;i++)for(j=0;j<G->numVertexes;j++)G->arc[i][j] = INFINITY;//邻接矩阵初始化for(k=0;k<G->numEdges;k++) //读入numEdges条边,建立邻接矩阵{printf("输入边(vi,vj)上的下标i,下标j和权w:\n");scanf("%d,%d,%d",&i,&j,&w); //输入边(vi,vj)上的权wG->arc[i][j] = w;G->arc[j][i] = G->arc[i][j];//因为是无向图,矩阵对称。}}



从代码中也可以得到,n个顶点和e条边的无向网图的创建,时间复杂度为O(n+n^2+e),其中对邻接矩阵G->arc的初始化耗费了O(n^2)的时间。

邻接表:
邻接矩阵对于边数相对顶点较少的图来说对存储空间是极大的浪费。
>>>考虑对边或弧使用链式存储的方式来避免空间浪费的问题。类比于树的孩子表示法,将结点存入数组,并对结点的孩子进行链式存储,不管有多少孩子,也不存在空间浪费的问题。

邻接表:数组与链表相结合的存储方法称为邻接表(Adjacency List)。
邻接表的处理办法:
1.图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以比较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
2.图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。

顶点表的各个结点由data 和 firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此结点的第一个邻接点。
边表结点由adjvex和next 两个域组成,adjvex 是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。

对于有向图,其实存储是类似的,但是要注意弧的方向。一般是以顶点为弧尾来存储边表,这样可以很容易就可以得到每个顶点的出度。
但是,相对的,也有以顶点为弧头来存储边表的,这样叫做逆邻接表:即对每个顶点vi都建立一个链接为vi为弧头的表。

以下是邻接表的具体示例图:


此时,我们很容易就可以计算出某个顶点的入度或出度为多少,判断两顶点是否存在弧也很容易实现。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可,如图所示:


下面关于结点定义的代码;
typedef char VertexType; //顶点类型,由用户定义typedef int EdgeType;typedef struct EdgeNode//边表结点{int adjvex;//邻接点域,存储该顶点对应的下标EdgeType weight;//用于存储权值,对于非网图可以不需要struct EdgeNode *next;//链域,指向下一个邻接点}EdgeNode;typedef struct VertexNode//顶点表结点{VertexType data;//顶点域,存储顶点信息EdgeNode *firstedge;//边表头指针}VertexNode,AdjList[MAXVEX];typedef struct{AdjList adjList;int numVertexes,numEdges;//图中当前顶点数和边数}GraphAdjList;对于邻接表的创建,代码如下:void CreateALGraph(GraphAdjList *G){int i,j,k;EdgeNode *e;printf("输入顶点数和边数:\n");scanf("%d,%d",&G->numVertexes,&G->numEdges);//输入顶点数和边数for(i=0;i<G->numVertexes,i++){scanf(&G->adjList[i].data);//输入顶点信息G->adjList[i].firstedge=NULL;//将边表置位空表}for(k=0;k<G->numEdges;k++){printf("输入边(vi,vj)上的顶点序号:\n");scanf("%d,%d",&i,&j);//输入边(vi,vj)上的顶点序号e=(EdgeNode *)malloc(sizeof(EdgeNode));/*向内存申请空间,生成边表结点*/e->adjvex = j;//邻接序号为je->next = G->adjList[i].firstedge;//将e指针指向当前顶点指向的结点<span style="white-space:pre"></span>G->adjList[i].firstedge = e;//将当前顶点的指针指向ee=(EdgeNode *) malloc(sizeof(EdgeNode));/*向内存申请空间,生成边表结点*/e=adjvex = i;//邻接序号为ie->next=G-adjList[j].firstedge;//将e指针指向当前顶点指向的结点G->adjList[j].firstedge = e;//将当前顶点的指针指向e}}



本算法的时间复杂度,对于n个顶点的e条边来说,很容易得出是O(n+e)

十字链表:
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。

把邻接表和逆邻接表结合起来得存储方式->>>十字链表(Orthogonal List)。

重新定义顶点表结点结构如下:
----------------------------
|data| firstin| firstout|
----------------------------
其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。

重新定义的边表结点结构如下:
------------------------------------------------
|tailvex | headvex | headlink | taillink |
-------------------------------------------------
其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标,headlink是指入边表指针,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以增加一个weight域来存储权值。

例子示例图:


十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到一vi为尾的弧,也容易找到vi为头的弧,因而容易求得顶点的出度和入度和出度。而且其除了结构复杂了一点之外,其实创建图算法的时间复杂度和邻接表相同,故在有向图的应用中,十字链表是非常好的数据结构模型。

邻接多重表:
由于使用邻接表的方式来说,对于无向图来说,删除某条边其实是比较麻烦的,所以我们可以对边表的结构来一些改造:

重新定义的边表结点结构如下:
-------------------------------
| ivex | ilink | jvex | jlink |
-------------------------------

其中ivex和jvex是与某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。这就是邻接多重表结构。

例子示例图(下面有两图):




邻接多重表和邻接表的区别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了。只需要将连向将要删除的边的指向改为^(置空)即可。

边集数组:
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。如图所示:


边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
关于边集数组的应用,在后面的克鲁斯卡尔(Kruskal)算法中有介绍->>>

定义的边数组结构如下所示:
----------------------------
| begin | end |weight |
----------------------------
其中begin是存储起点下标,end是存储终点下标,weight是存储权值。


图的遍历:
从图中的某一顶点出发,访遍图中其余顶点,且使每一顶点仅被访问一次,这一过程就叫做图的遍历(Traversing Graph).

对于图的遍历来说,如何避免因回回路陷入死循环,一般有两种遍历次序方案可以解决:深度优先遍历和广度优先遍历。

深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称为DFS。类似于树的前序遍历。
从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。
当然,以上说的只是连通图,对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问到位置。

邻接矩阵的方式,来遍历:
typedef int Boolean;//Boolean 是布尔类型,其值是TRUE或FALSEBoolean visited[MAX];//访问标志的数组/* 邻接矩阵的深度优先递归算法*/void DFS(MGraph G,int i){int j;visited[i]=TRUE;printf("%c",G.vexs[i]);//打印顶点,也可以其他操作for(j=0;j<G.numVertexes;j++)<span style="white-space:pre"></span>if(G.arc[i][j]==1&&!visited[j])DFS(G,j);//对为访问的邻接顶点递归调用}/*邻接矩阵的深度优先遍历*/void DFSTraverse(MGraph G){int i;for(i=0;i<G.numVertexes;i++)visited[i] = FALSE;for(i=0;i<numVertexes;i++)if(!visited[i])DFS(G,i);}



如果图结构为邻接表结构,其DFSTraverse函数的代码是几乎相同的,只是在递归函数中因为将数组换成了链表而有不同,代码如下:
/*邻接表的深度优先递归算法*/void DFS(GraphAdjList GL,int i){EdgeNode *p;visited[i] = TRUE;printf("%c",GL->adjList[i].data);//打印顶点,也可以其他操作p = GL->adjList[i].firstedge;while(p){if(!visited[p->adjvex])DFS(GL,p->adjvex);p = p->next;}}
/*邻接表的深度遍历操作*/void DFSTraverse(GraphAdjList GL)
{int i;for (i = 0;i<GL->numVertexes;i++)visited[i] = FALSE;//初始所有顶点状态都是未访问过状态for (i = 0;i<GL->numVertexes;i++)if(!visited[i])//对未访问过的顶点调用DFS,若是连通图,只会执行一次DFS(GL,i);}





关于以上两种存储结构的深度优先遍历算法的时间复杂度:
邻接矩阵:O(n^2)
邻接表:O(n+e)
显然,对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。

对于有向图而言,由于它只是对于通道存在可行或不可行,算法上没有变化,是完全可以通用的。



广度优先遍历:
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称为BFS。类似于树的层序遍历。

邻接矩阵的广度优先遍历算法:
void BFSTraverse(MGraph G){int i,j;Queue Q;for(i = 0;i<G.numVertexes;i++)visited[i] = FALSE;InitQueue(&Q);//初始化一辅助用的队列for(i=0;i<G.numVertexes;i++)//对每一个顶点做循环{if(!visited[i])//若是未访问过{visited[i] = TRUE;//设置当前顶点访问过printf("%c",G.vexs[i]);//打印顶点,也可以其他操作EnQueue(&Q,i);//将此顶点如队列while(!QueueEmpty(Q))//若当前队列不为空{DeQueue(&Q,&i);//将队列中元素出队列,赋值给ifor(j=0;j<G.numVertexes;j++){//判断其他顶点若与当前顶点存在边且未访问过if(G.arc[i][j]==1&&!visited[j]){visited = TRUE;//将找到的此顶点标记为已访问过printf("%c",G.vexs[j]);//打印顶点EnQueue(&Q,j);//将找到的此顶点入队列}}}}}}




对于邻接表的广度优先遍历,代码与邻接矩阵差异不大,如下:
/*邻接表的广度遍历算法*/void BFSTraverse(GraphAdjList GL){int i;EdgeNode *p;Queue Q;for(i=0;i<GL->numVertexes;i++)visited = FALSE;InitQueue(&Q);for(i = 0;i<GL->numVertexes;i++){if(!visited[i]){visited[i]=TRUE;printf("%c",GL->adjList[i].data);//打印顶点,也可以其他操作EnQueue(&Q,i);while(!QueueEmpty(Q)){DeQueue(&Q,&i);p = GL->adjList[i].firstedge;//找到当前顶点边表链表头指针while(p){if(!visited[p->adjvex)//若此顶点未被访问{visited[p->adjvex] = TRUE;printf("%c",GL->adjList[p->adjvex].data);EnQueue(&Q,p->adjvex);//将此顶点如队列}p = p->next;//指针指向下一个邻接点}}}}}



对比图的深度优先遍历和广度优先遍历算法,发现,其实两者在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问次序的不同,故其实两者没有优劣之分,只是视不同的情况选择不同的算法。


深度优先算法更适合目标比较明确的。以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
0 0
原创粉丝点击