大话数据结构之图(上)

Posted -恰饭第一名-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大话数据结构之图(上)相关的知识,希望对你有一定的参考价值。

图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关

一、图的定义

图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中的顶点的集合,E是图G中边的集合

在这里插入图片描述

  1. 线性表中数据元素成为元素,树中称为结点,在图中称为顶点
  2. 在定义中,若V是顶点的集合,则强调来了顶点集合V有穷非空
  3. 在图中,任意两个顶点都可能有关系,顶点之间的逻辑关系用边来表示

1、无向边

若顶点Vi到Vj之间的边没有方向,则称这条边为无向边,用无序偶对(Vi,Vj)来表示

在这里插入图片描述
在这里插入图片描述


2、有向边

若从顶点Vi到Vj的边有方向,则称这条边为有向边,也称为弧,用有序偶<Vi,Vj>来表示,Vi称为弧伟,Vj称为弧头

在这里插入图片描述
连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A,D>表示弧,不能写成<D,A>

3、简单图

若不存在顶点到其自身的边,且同一条边不重复出现

4、无向完全图

在无向图种,如果任意两个顶点之间都存在边,则称改图为无向完全图,n个顶点有n*(n-1)/2条边

在这里插入图片描述

5、有向完全图

在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称改图为有向完全图。n个顶点有n*(n-1)条边

在这里插入图片描述


有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight),这些权可以表示从一个顶点到另一个顶点的距离或耗费。
这种带权的图通 常称为网(Network)。
在这里插入图片描述
假设有两个图G= (V,{E})和G’ = (V’,{E’}),如果V’∈V且E’∈E,则称G’为G的子图

例如下面带底纹的图均为左侧无向图与有向图的子图
在这里插入图片描述



(2)图的顶点与边间关系

(3)连通图相关术语

在无向图G中,如果从顶点V到顶点V’有路径,则称V和V’是连通的。
如果对于图中任意两个顶点Vi,Vj∈V,Vi和Vj都是连通的,则称G是连通图

在这里插入图片描述
无向图中的极大连通子图称为连通分量

  • 要是子图
  • 子图要是连通的
  • 连通子图含有极大顶点数
  • 具有极大顶点数的连通子图包含依附于这些顶点的所有边

二、图的抽象数据类型

ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。
Operation
CreateGraph (*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是无向图,还需要增添对称弧
DeleteArc (*G,v,w):在图G中删除弧<v,w>,若G是无向图,则还删除对称弧
DFSTraverse ( G ):对图G中进行深度优先遍历,在遍历过程对每个顶点调用。 
HFSTraverse (G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用。 endADT



三、图的存储结构

3.1、邻接矩阵

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

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对称矩阵:n阶矩阵的元满足aij=aji(0<=i,j<=n),即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角对应的元全都是相等的

有了这个矩阵,我们就可以很容易地知道图中的信息

  1. 我们要判定任意两顶点是否有边无边就非常容易了
  2. 我们要只读某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如顶点V1的度就是1+0+1+0=2
  3. 求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描一遍, arc[i][j]为1就是邻接点



在这里插入图片描述

顶点数组为vertex[4]={ v0, v1, v2, v3},孤数组arc[4][4]为图7-4-3右图这样的一个 矩阵。主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称,比如由Vi 到Vo有弧,得到arc[1][0]=1,而v。V0到V1没有弧,因此arc[0][l]=1。

有向图讲究入度与出度,顶点V1的入度为1,正好是第V1列各数之和。顶点VI 的出度为2,即第V1行的各数之和。

与无向图同样的办法,判断顶点Vi到Vj是否存在弧,只需要査找矩阵中arc[i][j] 是否为1即可。要求用的所有邻接点就是将矩阵第i行元素扫描一遍,査找arc[i][j] 为1的顶点。

在图的术语中,我们提到了网的概念,也就是每条边上带有权的图叫做网。那么 这些权值就需要存下来,如何处理这个矩阵来适应这个需求呢?我们有办法。
在这里插入图片描述

这里Wij表示(Vi,Vj)或<Vi,Vj>上的权值。∞表示一个计算机允许的、大于所有边 上权值的值,也就是一个不可能的极限值。

如图7-4-4左图就是一个有向网图,右图就是它 的邻接矩阵。

在这里插入图片描述

#define MAXVEX 100 /* 最大顶点数,应由用户定义 */
#define GRAPH_INFINITY 65535 /* 用65535来代表∞ */

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");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
		scanf(&G->vexs[i]);
	for(i = 0;i <G->numNodes;i++)
		for(j = 0;j <G->numNodes;j++)
			G->arc[i][j]=GRAPH_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)上的权w */
		G->arc[i][j]=w; 
		G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
	}
}

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

3.2、邻接表

邻接表的处理办法:

  1. 图中顶点用一个一维数组存储,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
  2. 图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi的边表,有向图则称为顶点Vi作为弧尾的出边表

在这里插入图片描述

顶点表的各个顶点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点


边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针

比如V1顶点与V0,V2互为邻接点,则在V1的边表中,adjvex分别为V0的0和V2的2




获取某个顶点的度

去查找这个顶点的边表中结点的个数

判断顶点Vi到Vj是否存在边

只需要测试顶点Vi的边表中adjvex是否存在结点Vj的下标j就可以了

求顶点的所有邻接点

就是对此顶点的边表进行遍历,得到的adjvex域对应的顶点就是邻接点



若是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度

在这里插入图片描述

有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点Vi都建立一个链接为Vi为弧头的表

在这里插入图片描述

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



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

在这里插入图片描述


结点定义的代码

typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */

typedef struct EdgeNode /* 边表结点  */
{
	int adjvex;    /* 邻接点域,存储该顶点对应的下标 */
	EdgeType info;		/* 用于存储权值,对于非网图可以不需要 */
	struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;

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

typedef struct
{
	AdjList adjList; 
	int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

无向图的邻接表的创建

/* 建立图的邻接表结构 */
void  CreateALGraph(GraphAdjList *G)
{
	int i,j,k;
	EdgeNode *e;
	printf("输入顶点数和边数:\\n");
	scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
	for(i = 0;i < G->numNodes;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;					/* 邻接序号为j */                         
		e->next=G->adjList[i].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[i].firstedge=e;		/* 将当前顶点的指针指向e */               
		
		e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
		e->adjvex=i;					/* 邻接序号为i */                         
		e->next=G->adjList[j].firstedge;	/* 将e的指针指向当前顶点上指向的结点 */
		G->adjList[j].firstedge=e;		/* 将当前顶点的指针指向e */               
	}
}



3.3、十字链表

对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须 要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可 能把邻接表与逆邻接表结合起来呢?答案是肯定的,就是把它们整合在一起。

这就是 我们现在要讲的有向图的一种存储方法:十字链表(Orthogonal List)


重新定义顶点表结构

在这里插入图片描述

firstin表示入边表头指针,指向该顶点的入边表中的第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点


重新定义的边表结点结构

在这里插入图片描述

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

3.4、临界多重表

3.5、边集数组



四、图的遍历

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

4.1、深度优先遍历

深度遍历其实就是一个递归的过程,更像一棵树的前序遍历。

它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到


邻接矩阵的方式

typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
Boolean visited[MAXVEX]; /* 访问标志的数组 */

/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph G, int i)
{
	int j;
 	visited[i] = TRUE;
 	printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */
	for(j = 0; j < G.numVertexes; j++)
		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 < G.numVertexes; i++)
 		if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */ 
			DFS(G, i);
}


如果图结构是邻接表结构,其DFSTraverse函数的代码是几乎相同的,只是在递归函数中因为数组换成了链表而有不同

Boolean visited[MAXSIZE]; /* 访问标志的数组 */

/* 邻接表的深度优先递归算法 */
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);
}


4.2、广度优先遍历

类似树的层级遍历

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


邻接矩阵结构的广度优先遍历算法

/* 邻接矩阵的广度遍历算法 */
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);	/* 将队对元素出队列,赋值给i */
				for(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);				/* 将找到的此顶点入队列  */
					} 
				} 
			}
		}
	}
}



邻接表的广度优先遍历

/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
	int i;
    EdgeNode *p;
	Queue Q;
	for(i = 0; i < GL->numVertexes; i++)
       	visited[i] = 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;	/* 指针指向下一个邻接点 */
				}
			}
		}
	}
}

深度优先更适合目标比较明确,以找到目标为主要目的的情况

广度优先更适合在不断扩大遍历范围时找到相对最优解的情况

以上是关于大话数据结构之图(上)的主要内容,如果未能解决你的问题,请参考以下文章

大话数据结构之图(下)

数据结构实验之图论二:图的深度遍历-java代码

c++实现图的表示,数据结构之图

c++实现图的表示,数据结构之图

SDUT 3363 数据结构实验之图论七:驴友计划

数据结构之图2