【第七章:图】

知识框架

在这里插入图片描述

No.1图的基本概念

线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

一、图的定义

在这里插入图片描述

注意:线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。

二、图的基本概念和术语

1、有向图

若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。

在这里插入图片描述

图(a)所示的有向图G 1 :

在这里插入图片描述

2、无向图

若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v, w)或(w,v),因为(v,w)=(w,v), 其中v,w是顶点。可以说顶点w和顶点v互为邻接点。边(v, w)依附于顶点w和v,或者说边(v, w)和顶点v, w相关联。

在这里插入图片描述

图(b)所示的无向图G2可表示为:

在这里插入图片描述

3、简单图

一个图G若满足:①不存在重复边;②不存在顶点到自身的边,则称图G 为简单图。上图中G1和G2均为简单图,。数据结构中仅讨论简单图。

4、多重图

若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图。多重图的定义和简单图是相对的。

5、完全图(也称简单完全图)

在这里插入图片描述

在这里插入图片描述

6、子图

在这里插入图片描述

注意:并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图,即E的子集中的某些边关联的顶点可能不在这个V的子集中。

7、连通、连通图和连通分量

在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G 中任意两个顶点都是连通的,则称图G 为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。若一个图有n个顶点,并且边数小于n − 1,则此图必是非连通图。如下图(a)所示, 图G4有3个连通分量,如图(b)所示。

在这里插入图片描述

注意:弄清连通、连通图、连通分量的概念非常重要。首先要区分极大连通子图和极小连通子图,极大连通子图是无向图的连通分量,极大即要求该连通子图包含其所有的边;极小连通子图是既要保持图连通又要使得边数最少的子图。

8、强连通图、强连通分量

在有向图中,若从顶点v到顶点w和从顶点w到项点v 之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量,图G1的强连通分量如下图所示。

在这里插入图片描述

注意:强连通图、强连通分量只是针对有向图而言的。一般在无向图中讨论连通性,在有向图中考虑强连通性。

9、生成树、生成森林

连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n ,则它的生成树含有n − 1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。图G2 的一个生成树如下图所示。

在这里插入图片描述

注意:包含无向图中全部顶点的极小连通子图,只有生成树满足条件,因为砍去生成树的任一条边,图将不再连通。

10、顶点的度、入度和出度

在这里插入图片描述

11、边的权和网

在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称

12、稠密图、稀疏图

边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。一般当图G满足∣E∣<∣V∣log∣V∣时,可以将G视为稀疏图。

13、路径、路径长度和回路

顶点 v p v_p vp到顶点 v q v_q vq之间的一条路径是指顶点序列 v p v_p vp, v i 1 v_{i1} vi1, v i 2 v_{i2} vi2, v i 3 v_{i3} vi3…, v i m v_{im} vim, v q v_q vq, ,当然关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n 个顶点,并且有大于n − 1条边,则此图一定有环。

14、 简单路径、简单回路

在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。

15、距离

从顶点u出发到顶点v 的最短路径若存在,则此路径的长度称为从u 到v 的距离。若从u 到v根本不存在路径,则记该距离为无穷( ∞ ) 。

16、有向树

一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。

No.2图的存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。而多重链表的方式,要么会造成很多存储单元的浪费,要么又带来操作的不便。因此,对于图来说,如何对它实现物理存储是个难题,接下来我们介绍五种不同的存储结构。

一、邻接矩阵

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

设图G 有n个顶点,则邻接矩阵A是一个n ∗ n 的方阵,定义为:

在这里插入图片描述

下图是一个无向图和它的邻接矩阵:

在这里插入图片描述

可以看出:

  1. 无向图的邻接矩阵一定是一个**对称矩阵(**即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
  2. 对于无向图,邻接矩阵的第i 行(或第i 列)非零元素(或非∞ 元素)的个数正好是第i 个顶点的度TD( v i v_i vi).比如顶点 v 1 v_1 v1的度就是1+0+1+0=2
  3. 求顶点 v i v_i vi的所有邻接点就是将矩阵中第i行元素扫描一遍,A[i][j]为1就是邻接点、

下图是有向图和它的邻接矩阵:

在这里插入图片描述

可以看出:

  1. 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
  2. 有向图讲究入度与出度,顶点 v 1 v_1 v1的入度为1,正好是第 v 1 v_1 v1列各数之和,,顶点 v 1 v_1 v1的出度为2,即第 v 1 v_1 v1行的各数之和。
  3. 与无向图同样的办法,判断顶点 v i v_i vi v j v_j vj是否存在弧,只需要查找矩阵中A[i][j]是否为即可。

对于带权图而言,若顶点 v i v_i vi v j v_j vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值:

在这里插入图片描述

下图是有向网图和它的邻接矩阵:

在这里插入图片描述

通过以上对无向图、有向图和网的描述,可定义出邻接矩阵的存储结构:

#define MaxVertexNum 100	//顶点数目的最大值
typedef char VertexType;	//顶点的数据类型
typedef int EdgeType;	//带权图中边上权值的数据类型
typedef struct{
	VertexType Vex[MaxVertexNum];	//顶点表
	EdgeType Edge[MaxVertexNum][MaxVertexNum];	//邻接矩阵,边表
	int vexnum, arcnum;	//图的当前顶点数和弧树
}MGraph;

①在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。
②当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。
③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
④邻接矩阵表示法的空间复杂度为O( n 2 n^2 n2), 其中n为图的顶点数|V|.。。
⑤ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
⑥ 稠密图适合使用邻接矩阵的存储表示。

二、邻接表

当一个图为稀疏图时(边数相对顶点较少),使用邻接矩阵法显然要浪费大量的存储空间,如下图所示:

在这里插入图片描述

而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图G中的每个顶点 v i v_i vi建立一个单链表,第i个单链表中的结点表示依附于顶点 v i v_i vi的边(对于有向图则是以顶点 v i v_i vi为尾的弧),这个单链表就称为顶点 v i v_i vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点,如下图所示。

在这里插入图片描述

顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。
无向图的邻接表的实例如下图所示。
在这里插入图片描述
有向图的邻接表的实例如下图所示。

在这里插入图片描述

此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。

图的邻接表存储结构定义如下:

#define MAXVEX 100	//图中顶点数目的最大值
type char VertexType;	//顶点类型应由用户定义
typedef int EdgeType;	//边上的权值类型应由用户定义
/*边表结点*/
typedef struct EdgeNode{
	int adjvex;	//该弧所指向的顶点的下标或者位置
	EdgeType weight;	//权值,对于非网图可以不需要
	struct EdgeNode *next;	//指向下一个邻接点
}EdgeNode;

/*顶点表结点*/
typedef struct VertexNode{
	Vertex data;	//顶点域,存储顶点信息
	EdgeNode *firstedge	//边表头指针
}VertexNode, AdjList[MAXVEX];

/*邻接表*/
typedef struct{
	AdjList adjList;
	int numVertexes, numEdges;	//图中当前顶点数和边数
}

图的邻接表存储方法具有以下特点

  1. 若G为无向图,则所需的存储空间为O(|V|+2|E|);若G为有向图,则所需的存储空间为O(|V|+|E|).前者的倍数2是由于无向图中,每条边在邻接表中出现了两次
  2. 对于稀疏图,采用邻接表表示将极大地节省存储空间。
  3. 在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为 O(n).。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
  4. 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。当然,这实际上与邻接表存储方式是类似的。
  5. 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

三、十字链表

十字链表是有向图的一种链式存储结构。
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可能把邻接表与逆邻接表结合起来呢?答案是肯定的,就是把它们整合在一起。这就是我们现在要介绍的有向图的一种存储方法:十字链表(Orthogonal List)。
我们重新定义顶点表结点结构如下表所示。

在这里插入图片描述

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

在这里插入图片描述

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

接下来通过一个例子详细介绍十字链表的结构。

如下图所示,顶点依然是存入一个一维数组{ v 0 v_0 v0, v 1 v_1 v1, v 2 v_2 v2, v 3 v_3 v3}.实线箭头指针的图示完全与的邻接表的结构相同。就以顶点 v 0 v_0 v0来说,firstout 指向的是出边表中的第一个结点V3.所以V0边表结点的headvex = 3;而tailvex就是当前顶点V0的下标0,由于V0只有一个出边顶点,所以headlink和taillink都是空。

在这里插入图片描述

在这里插入图片描述

四、邻接多重表

邻接多重表是无向图的另一种链式存储结构。
在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。比如下图中,若要删除左图的(V0,V2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较烦琐的。

在这里插入图片描述

重新定义的边表结点结构如下表所示。

在这里插入图片描述

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

每个顶点也用一一个结点表示,它由如下所示的两个域组成。

在这里插入图片描述
其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。

我们来看结构示意图的绘制过程,理解了它是如何连线的,也就理解邻接多重表构造原理了。如下图7所示,左图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点画出来。

在这里插入图片描述

在这里插入图片描述

到这里,可以明显的看出,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。 这样对边的操作就方便多了,若要删除左图的(V0,V2)这条边,只需要将右图的⑥⑨的链接指向改为NULL即可。

五、边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、 终点下标(end)和权(weight)组成,如下图所示。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。

在这里插入图片描述

No.3图的遍历

图的遍历是和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次, 这一过程就叫做图的遍历(Traversing Graph)。
对于图的遍历来,通常有两种遍历次序方案:它们是深度优先遍历和广度优先遍历。

一、深度优先遍历

深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。

1、DFS算法

深度优先搜索类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图。它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点W1 ,再访问与W1 ​邻接且未被访问的任一顶点…重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。

般情况下,其递归形式的算法十分简洁,算法过程如下:

bool visited[MAX_VERTEX_NUM];	//访问标记数组
/*从顶点出发,深度优先遍历图G*/
void DFS(Graph G, int v){
	int w;
	visit(v);	//访问顶点
	visited[v] = TRUE;	//设已访问标记
	//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
	//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
	for(w = FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w)){
		if(!visited[w]){	//w为u的尚未访问的邻接顶点
			DFS(G, w);
		}
	}
}
/*对图进行深度优先遍历*/
void DFSTraverse(MGraph G){
	int v; 
	for(v=0; v<G.vexnum; ++v){
		visited[v] = FALSE;	//初始化已访问标记数据
	}
	for(v=0; v<G.vexnum; ++v){	//从v=0开始遍历
		if(!visited[v]){
			DFS(G, v);
		}
	}
}

以下面这个无向图为例:其深度优先遍历的结果为a b d e h c f g

在这里插入图片描述

2、DFS算法的性能分析

DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为O(V)
对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O( V 2 V^2 V2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O ( V + E ) .显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,是完全可以通用的。

3、深度优先的生成树和生成森林

深度优先搜索会产生一棵深度优先生成树。 当然,这是有条件的,即对连通图调用DFS才能产生深度优先生成树,否则产生的将是深度优先生成森林,如下图所示。基于邻接表存储的深度优先生成树是不唯一的 。

在这里插入图片描述

二、广度优先遍历

广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。

1、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]){
			vivited[i] = TRUE;	//设置当前访问过
			visit(i);	//访问顶点
			EnQueue(&Q, i);	//将此顶点入队列
			//若当前队列不为空
			while(!QueueEmpty(Q)){
				DeQueue(&Q, &i);	//顶点i出队列
				//FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。
				//NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v
				for(j=FirstNeighbor(G, i); j>=0; j=NextNeighbor(G, i, j)){
					//检验i的所有邻接点
					if(!visited[j]){
						visit(j);	//访问顶点j
						visited[j] = TRUE;	//访问标记
						EnQueue(Q, j);	//顶点j入队列
					}
				}
			}
		}
	}
}

以下面这个无向图为例:其广度优先遍历的结果为a b c d e f g h
在这里插入图片描述

2、BFS算法性能分析

无论是邻接表还是邻接矩阵的存储方式,BFS 算法都需要借助一个辅助队列Q, n个顶点均需入队一次,在最坏的情况下,空间复杂度为O(V).
采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次), 在搜索任一顶点的邻接点时,每条边至少访问一次,算法总的时间复杂度为O ( V + E )
采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为O(V),故算法总的时间复杂度为O(V^2);

注意:图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同。因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于邻接表的遍历所得到的DFS序列和BFS序列是不唯一的。

三、图的遍历与图的连通性

图的遍历算法可以用来判断图的连通性。
对于无向图来说,若无向图是连通的,则从任一结点出发, 仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点。
故在BFSTraverse ()或DFSTraverse ()中添加了第二个for循环,再选取初始点,继续进行遍历,以防止一次无法遍历图的所有顶点。对于无向图,上述两个函数调用BFS (G,i)或DFS(G,i)的次数等于该图的连通分量数;而对于有向图则不是这样,因为一个连通的有向图分为强连通的和非强连通的,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用BFS (G, i)或DFS (G, i)无法访问到该连通分量的所有顶点。
如下图所示为有向图的非强连通分量。

在这里插入图片描述

No.4最小生成树

一、普里姆(Prim)算法

二、克鲁斯卡尔(Kruskal)算法

No.5最短路径

一、迪杰斯特拉( Dijkstra )算法

二、弗洛伊德( Floyd )算法

No.6拓扑排序

一、定义

二、算法

No.7关键路径

一、定义

二、算法

No.8总结

2.物理结构(存储结构)

三、抽象数据类型

1.数据类型

2.抽象数据类型

No.2算法

一、算法的定义

二、算法特性

三、函数的渐进增长

四、算法时间复杂度

1.算法时间复杂度定义

2.推导大O阶方法

3.最坏情况和平均情况

4.算法空间复杂度

附录

上文链接

下文链接

专栏

参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值