数据结构之图
图(graph)
一、图的定义
用顶点、边构成的存储结构。有:
G=(V,E)(graph=(Vertex,Edge))
Vertex就是顶点的有穷非空集合,Edge就是边的的 有穷集合。
二、图的术语
- 有向图/无向图:无向图中的边叫边,有向图中的边叫弧

- 完全图:对无向图取任意两个顶点都有一条边相连,n(n-1)/2条;对有向图取任意两个顶点都有两条不同方向的弧,n(n-1)条。

- 稀疏图:有很少边或弧的图。(个数<nlog2n)
- 稠密图:有较多或弧的图
- 网:边/弧带权的图
- 邻接:两个顶点的关系,无向图中称A与B互为邻接;有向图中称A邻接到B,B邻接于A。
- 关联:边/弧与顶点之间的关系。一条边/弧连接两顶点,则称该边/弧关联于这两个顶点
- 顶点的度:与该顶点相关联的边/弧数目。有向图中,顶点的度等于入度和出度之和。
- 路径:接续的边构成的顶点序列
- 路径长度:路径上边/弧的数目/权值之和。
- 回路(环):第一个顶点和最后一个顶点相同的路径
- 简单路径:各顶点均不相同的路径。
- 简单回路:除起点顶点,其余顶点均不相同的回路。
- 连通图:任意两顶点都有路径的图。有向图中连通图称为强连通图。
- 子图:如下

- 极大连通子图:该子图是G连通子图,将G的任何不在该子图的顶点加入,子图不再连通。
- (强)连通分量:图的极大连通子图。
- 极小连通子图:该子图是G连通子图,将该子图中删除任何一条边,子图不再连通。
三、图的类型和基本操作
图的类型:
数据对象:顶点集
数据关系:边的关系
重要的基本操作:
- CreateGraph(&G,V,VR)
初始条件:V是图的顶点集,VR是图中弧的集合
操作结果:按V和VR的定义构造图G
- DFStraverse(G)
初始条件:图G存在
操作结果:对图进行深度优先遍历
- BFSTraverse(G)
初始条件:图G存在
操作结果:对图进行广度优先遍历
四、图的存储结构
图没有顺序存储结构,但图和树、栈、队列等一样,都具有数组表示法和链式存储表示法。
1. 数组表示法
(1)定义
主要是利用了邻接矩阵来存储了边与点之间的关系。那么何为邻接矩阵呢?,邻接矩阵是一个二维数组,
无向图如下:

上图红框处即为邻接矩阵。举个例子,因为A与B有边,所以arcs[0][1]=1,其余同。无向图的邻接矩阵是对角线对称的。
有向图如下:

有向图的邻接矩阵是不对称的。举个例子,因为A有指向B的边,所以arcs[0][1]=1,而B没有指向A的边,所以arcs[1][0]=0,其余同。
如果是网,无论是有向网还是无向网,都只是将上述的0换成无穷,1换成边上的权值。
(2)数组表示法下的图的类型定义和初始化(以有向网为例)
图的类型定义包括:
一维数组:存储顶点信息
邻接矩阵:存储边信息
数:存储图的当前顶点数和边数
图的初始化:
输入顶点数、边数
初始化邻接矩阵
输入哪两个顶点邻接与其边上的权值,找到两个顶点下标,在邻接矩阵中赋权值。
注意:创建无向和有向的唯一区别在于邻接矩阵不同,图和网唯一区别在于边上有无权值,程序修改很容易
程序如下:
#define MVNum 100 //最大顶点数,Max Vertex Number #define MaxInt 32767 //表示极大值,即∞ typedef char VerTexType //设顶点的数据类型为字符型 typedef int ArcType //假设边的权值类型为整型 typedef int Status typedef struct { VerTexType vexs[MVNum]; //顶点表 ArcType arcs[MVNum][MVNum]; //邻接矩阵 int vernum,arcnum; //图的当前点数和边数 }AMGraph,*PAMGraph;//Adjacency Matrix graph 邻接矩阵表示的图 Statue LocateVex(PAMGraph G,VerTexType v) { int i; for(i=0;i<G->vernum;i++) { if(G->vexs[i]==v) return i; } return 0; } /*以创建无向网为例*/ Status CreateUDN(PAMGraph G) //创建无向网,unsigned direction net { int i,j,w; VerTexType v1,v2; scanf("%d",&(G->vernum)); //输入总顶点数 scanf("%d",&(G->arcnum)); //输入总边数 for(i=0;i<vernum;i++) //输入顶点表信息 scanf("%d",&(G->vex[i])); for(i=0;i<vernum;i++) //初始化邻接矩阵 for(j=0;j<vernum;j++) G->vexs[i][j]=MaxInt; scanf("%c",&v1); //输入两个顶点信息 scanf("%c",&v2); scanf("%d",&w); //输入权值 i=LocateVex(G,v1); //找到顶点信息的下标 j=LocateVex(G,v2); G->arcs[i][j]=w; G->arcs[j][i]=w; //因为无向图对角线对称 return 1; }
2. 链式表示法
(1)定义
主要是利用了链表表示了边与点的关系,即顶点按编号顺序将顶点数据存储在一维数组中,链表存储该顶点邻接的所有顶点。如无向图如下

左边的即为链式表示法的示意图,左边存储顶点信息和邻接顶点的头指针,右边存储邻接顶点。如果是网,右边内部可以多加一位存储权值。
有向图如下:

有向图右边的链表只存储出度部分。比如顶点C,C的出度只有一条,指向B,所以C仅仅指向了一个。
(2)链式表示法下的图的类型定义和初始化(以无向图为例)
图的类型定义包括:
顶点信息和头指针、指针、节点数目和边数目
图的初始化:
输入顶点数和总边数
输入顶点信息和指针初始化
循环边数,输入哪两个顶点有边,找到这两个顶点下标,连接,
注意:以无向图为例,以链式表示法表示有向图、有向网、无向网等很容易编程。
程序如下:
#define MVNum 100 //最大顶点数,Max Vertex Number #define MaxInt 32767 //表示极大值,即∞ typedef char VerTexType //设顶点的数据类型为字符型 typedef int ArcType //假设边的权值类型为整型 typedef int Status typedef int Otherinfo typedef struct VNode { VerTexType data; //顶点信息 ArcNode *firstarc; //指向第一条依附该节点的边的指针 }VNODE,AdjList[MVNum]; //例如AdjList v;相当于 VNODE V[MVNum]; typedef struct ArcNode { ArcType adjvex; //边信息 Otherinfo info; //权值等其它信息 struct ArcNode *nextarc; //单链表开头 }ArcNode,*PArcNode; typedef struct { AdjList vertices; //顶点 int vexnum,arcnum; //顶点数目、边数目 }ALGraph,*PALGraph; int LocateVex(PALGraph G,VerTexType v) { int i; for(i=0;i<G->vexnum;i++) if(G->vexnum[i]->data==v) return i; return 0; } //采用邻接表表示法创建无向网 //输入总顶点数和总边数、建立顶点表、建立邻接表 Status CreateUDG(PALGraph G) { int i,j; int k; VerTexType v1,v2; scanf("%d",&(G->vexnum));//输入顶点数 scanf("%d",&(G->arcnum));//输入边数 for(i=0;i<G->vexnum;i++)//输入顶点信息 { scanf("%c",&G->vertices[i]->data); G->vertices[i]->firstarc=NULL;//指针初始化 } for(k=0;k<G->arcnum;k++) { scanf("%c",&v1);//输入哪两个顶点有边 scanf("%c",&v2); i=LocateVex(G,v1);//查找下标 j=LocateVex(G,v2); PArcNode p1; p1=(PArcNode)malloc(sizeof(ArcNode)); if(!p1) exit(-1); p1->adjvex=j; p1->nextarc=G->vertices[i]->firstarc; //相当于让链表的next等于null; G->vertices[i]->firstarc=p1;//连接 PArcNode p2; //无向网需要这一步,有向不需要 p2=(PArcNode)malloc(sizeof(ArcNode)); if(!p2) exit(-1); p2->adjvex=i; p2->nextarc=G->vertices[j]->firstarc; //相当于让链表的next等于null; G->vertices[j]->firstarc=p2; } return 1; }
(3)链式表示法的改进
对于有向图而言,很明显可以发现求顶点的度比较难,只知道出度,入度要遍历全部才能知道,因此我们有两种解决方法。一种是再做逆邻接表,存储入度部分;一种是采用十字链表存储,即逆邻接表和正邻接表结合在一起。如下

(懒来了,不想作图了,直接上青岛大学王卓老师的PPT了)
对于无向图而言,很明显可以发现每两条边都要存储两边,比较浪费内存,且增删边操作都挺难的,所以我们采用邻接多重表解决。邻接多重表如下:

3. 数组表示法和链式表示法的区别
邻接矩阵:
优点:直观,简单,好理解,方便检查任意一对顶点间是否存在边。
缺点:插入删除都不太方便,要重新改动邻接矩阵;对于稀疏图来说浪费空间;一定程度上浪费时间
邻接表
优点:相比邻接表,更适合用于稀疏图的存储。
缺点:实现难度有点复杂呀
五、图的遍历
1. 定义
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且每个顶点仅被访问一次,就叫做图的遍历啊。由于对某些回路来说,一个顶点可能会被访问多次,所以采用一个辅助数组来防止顶点重复访问
遍历实质:找到邻接顶点的过程
运用较多的遍历方法有两种:
深度优先遍历(DFS, depth_first search)
广度优先遍历(BFS,breadth_first search)
2. DFS(邻接矩阵遍历时间复杂度:O(n^2),邻接表:O(n+e),n顶点数,e为表节点)
简单一句话:一条路走到黑

比如存在上述无向图,以D为起点,先访问D,然后D有两个顶点A和C,然后选择访问C,C又有两个顶点B和E,然后选择访问B,B又有两个顶点C和A,C已经访问过了,只能访问A了,A又有两个顶点D和B,D和B都访问过了,此时返回到B,B邻近节点都访问过了,只有再返回到C节点,发现E还没访问,所以最后接着访问E顶点。
每次访问到一个没有访问过的顶点,就让辅助数组置1.
DFS遍历程序如下:(递归实现邻接矩阵)
void DFS(AMGraph G,int i) //图G和起始顶点 { int j; printf("第%d个顶点已访问\n",i); visited[i]=1; //辅助数组判断点是否重复访问 for(j=0;j<G->vernum;j++) if((G->arcs[i][j])!=0 && (!visited[j])) DFS(G,j); //如果第j个是第i个的邻接点,如果j未访问,则递归调用DFS return; }
3. BFS(邻接矩阵:O(n^2),邻接表:O(n+e),n顶点数,e为表节点)
简单一句话:一次性走完所有邻接点
方法:从图的某一点节点出发, 首先依次访问该节点的所有邻接点,再按这些顶点数被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点。重复此过程,直至所有顶点被访问为止。
BFS遍历程序如下:(非递归实现邻接表,运用了辅助队列缓冲)
void BFS(Graph G,int v) //按广度优先非递归遍历连通图G { printf("第%d个顶点被访问\n",v); //访问第v个顶点 visited[v]=1; InitQueue(Q); //辅助队列Q初始化 EnQueue(Q,v); //v进队 while(!QueueEmpty(Q)) //队列非空 { DeQueue(Q,u); //队头元素出队并置为u for(w=FirstAdjVex(G,u);w>=0;w=NexAdjVex(G,u,w))//w为u的尚未访问的邻接顶点 if(!visited[w]) { printf("第%d个顶点被访问\n",w); visited[w]=1; EnQueue(Q,w); //w进队 } } }
六、图的应用
最小生成树、最短路径、拓扑排序、关键路径
1. 最小生成树(针对无向图)
- 生成树:所有顶点均由边连接在一起,但不存在回路的图。如:

- 生成树的特点:
1. 一个图可以有许多棵不同的生成树;
2. 生成树的顶点个数与图的顶点个数相同;
3. 生成树是图的极小连通子图,去掉一条边则非连通;
4. 一个有n个顶点的连通图的生成树有n-1条边
5. 在生成树中任意两个顶点间的路径是唯一的;
6. 含n个顶点和n-1条边的图不一定是生成树
- 怎样构成一个图的生成树
设图G是个连通图,当从图任意一顶点出发遍历图G时,将边集合分成了两个集合T和B。其中T是遍历时候所经过的边的集合,B是没经过的边的集合。显然,遍历后访问的轨迹就是图G的极小连通子图,即子图G1是连通图G的生成树。(如下)

- 最小生成树:即在所有生成树里面,权值之和最小的生成树,也叫最小代价生成树。
- 典型用途带入:在n个城市间建立通信网,则n个城市应铺n-1条路,寻找最短成本路径。
- MST:
构造最小生成树的算法很多,其中多数算法都利用了MST(Minimum Spanning Tree)性质。设N=(V,E)是一个连通网,U是顶点集V的一个非空子集。若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在包含边(u,v)的最小生成树。例如存在下列无向图,求其最小生成树:

首先让U集合包含A顶点在结合内,V-U集合包含B,C,D,E,F,此时U与V-U集合相连的边权值分别为6,1,5,选择权值最小的1,然后将C纳入U集合,此时U集合包含A,C,V-U集合包含B,D,E,F,此时U与V-U集合相连的边权值分别为6,5,5,5,6,4,选择权值最小的4,然后将F纳如U集合,V-U集合包含B,D,E,依次类推,直至所有顶点都被访问一遍。
- 利用普利姆(Prim)算法的算法思想:

用人话来讲和我上面分析的过程差不多一样。
- 利用克鲁斯卡尔(Kruskal)算法的算法思想:

用人话来讲就是先把顶点和边分别取出,然后将边排序,然后在从权值最小的开始选n-1条,选的时候要判断是否会生成环,如果会则扔掉,继续选权值最小的边。
- 两类算法比较
Prim:O(n^2),用于稠密图
Kruskal: O(elog2e) ,用于稀疏图
2. 最短路径(针对有向图)
- 典型用途带入:
怎么从一个顶点到另外一个顶点最快,比如坐地铁啥的。最短路径就是最快的路径。
- 最短路径问题可总结为两类问题:
一个源点到其余各点的距离(迪杰斯算法解决) 和 图中各顶点之间最短路径(弗洛伊德算法解决)
- 迪杰斯特拉算法伪算法详解
专业版看不懂,只讲人话版。

如图中所言,求源点到其它各点最短路径要用一个辅助数组保存。假设有一个集合V,刚开始包含起点v0,进行第一轮比较,v0可以直接通过一条路径到达v1,v2,v4,v6,数组记录分别对应位置下的权值是13,8,30,32,v0不能直接通过一条路径到达v3,v5,则数组记录对应位置下的∞,然后此时选择直接路径到达最小的权值8,也就是v0到v2这条路径,然后将v2加入到集合V,然后进行第二轮比较,依次类推。注意,之后的比较需要考虑v2的存在。
最后可以求出v0到各个顶点的距离。
- 弗洛伊德算法简介:
数组插入法解决。

3. 拓扑排序(针对有向无环图)
- 有向无环图常用来描述一个工程或系统的进行过程,示意图如下:

- 有向无环图分为两类:AOV网和AOE网
AOV网(activity on vertex):顶点表示活动,边表示两个活动直接的制约关系,如下:

AOE网(activity on edge):边表示活动,两个顶点表示活动开始和结束,如下:

- 将有向无环图变成线性序列,这个序列为拓扑序列,这个变化过程为拓扑排序。
- 拓扑排序伪算法详解:
在有向图中选一个没有前驱的顶点且输出之
从图中删除该顶点和所有以它为尾的弧
重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止
此类算法避免了图中存在环的情况
4. 关键路径(以AOE网)
略,要用的时候再看,目前没什么地方用到。不过要有四个参数才能求关键路径。

浙公网安备 33010602011771号