数据结构与算法——图

0. 重要概念

图是常用的重要的一类数据结构,上一章的树可以看成是图的特例,树中每个数据元素至多允许一个前驱,只能反映数据元素之间一对多的关系,而图中没有该限制,允许数据元素可以有多个前驱,因此可以反映数据元素之间多对多的关系。

  • 边和顶点的关系
    设n为顶点数, e为边或弧的条数
    无向图: 0 <= e <= n(n-1)/2
    有向图: 0 <= e <= n(n-1)
  • 完全图
    无向完全图:边数为 n(n-1)/2 的无向图
    有向完全图:弧数为 n(n-1) 的有向图
  • 顶点的度TD(V)
    无向图:为依附于顶点V的边数
    有向图:等于以顶点V为弧头的弧数(称为V的入度,记为ID(V))与以顶点V为弧尾的弧数(称为V的出度,记为OD(V) )之和。即: TD(V) = ID(V) + OD(V)
  • 顶点连通
    若顶点v到顶点v’ 有路径,则称顶点v与v’ 是连通的
  • 连 通 图 : 包括无向连通图和有向连通图
    无向图:若图中任意两个顶点vi,vj都是连通的,则称该图是连通图(vi<>vj)
    有向图:若图中任意两个顶点vi,vj,都存在从vi到vj和从vj到vi的路径,则称该有向图为强连通图(vi<>vj)
  • 连通分量:
    无向图:无向图中极大连通子图,称为连通分量
    有向图:有向图中极大强连通子图,称为强连通分量

1.邻接矩阵

1.1 无权图的邻接矩阵

设图G=(V, {E})有n个顶点,则G的邻接矩阵定义为n阶方阵A。由于比较简单,直接举例如下:

在这里插入图片描述
有向图和无向图的数组表示法

  • 邻接矩阵的特点:
    • 判定两个顶点Vi与Vj是否关联, 只需判A[i,j]是否为1
    • 求顶点的度容易。

1.2 顶点的度

  • 无向图在这里插入图片描述
    即顶点Vi的度等于邻接矩阵中第i行(或第i列)的元素之和(非0元素个数)。
  • 有向图在这里插入图片描述在这里插入图片描述
即顶点Vi的出度为邻接矩阵中第i行元素之和; 顶点Vi的入度为邻接矩阵中第i列元素之和。

1.3 带权图的邻接矩阵

例如:


在这里插入图片描述

1.4 邻接矩阵代码实现

//网络的邻接矩阵的定义:
#define MaxVerterNum 100
typedef char VerterType;
typedef int EdgeType;
typedef struct{
	VerterType vexs[MaxVerterNum];//存储顶点的一维数组
	EdeType edges[MaxVerterNum][MaxVerterNum];//
	int n,e; //图当前的顶点数和边数
} MGragh;

//建立无向网络邻接矩阵的算法:
Void createMGragh(MGragh *G){
	int i,j,k,w;
	scanf(%d%d”,&G->n,&G->e); //读入顶点数和边数
	for(i=0;i<G->n;i++) //读入顶点信息,建立顶点表
		G->vexs[i]=getchar( );
	for(i=0;i<G->n;i++) //邻接矩阵初始化
		for(j=0;i<G->n;j++)
			G->edges[i][j]=0;
	for(k=0;k<G->e;k++){ //读入e条边,建立邻接矩阵
		scanf(%d%d”,&i,&j); //读入边<vi,vj>上的权w
		G->edges[i][j]=1;
		G->edges[j][i]=1;
	}
}

2. 邻接表

2.1 无向图邻接表

对图中每个顶点Vi建立一个单链表,链表中的结点表示依附于顶点Vi的边,每个链表结点(弧结点)为两个域。

AdjVex(邻接点域)NextArc (链域)
记载与顶点Vi邻接的顶点信息指向下一个与顶点Vi邻接的链表结点

每个链表附设一个头结点(顶点结点),头结点结构为:

VexDataFirstArc
存放顶点信息(姓名、编号等)指向链表的第一个结点

代码如下:

//弧结点
struct edge
{
	int v;
	struct edge *nextarc;
};
//顶点结点
struct vex
{
	ElemType data;
	struct edge *firstarc;
}

举例1:


在这里插入图片描述

举例2:

在这里插入图片描述

无向图邻接表特点

  • n个顶点, e条边的无向图, 需n个头结点和2e个链表结点
  • 顶点Vi的度 TD(Vi) = 链表i中的链表结点数

2.2 有向图邻接表

2.2.1 有向图邻接表

与无向图的邻接表结构一样。只是在第i条链表上的结点是以Vi为弧尾的各个弧头顶点,即出边表
举例:

在这里插入图片描述

有向图邻接表特点

  • n个顶点, e条弧的有向图,需n个表头结点, e 个链表结点
  • 第i条链表上的链表结点数,为Vi的出度(求顶点的出度易,求入度难)

2.2.2 有向图逆邻接表

与无向图的邻接表结构一样。只是在第i条链表上的结点是以Vi为弧头的各个弧尾顶点,即入边表
举例:

在这里插入图片描述

此时,第i条链表上的结点数,为Vi的入度。

2.2.3 代码实现

//邻接表的结构定义和建立算法:
typedef struct node{ //边表结点
	int adjtex; //邻接点域
	struct node *next; //链域
}EdgeNode; //若也表示边上的权,增加一个数据域

typedef struct vnode{ //顶点表结点
	VertexType vertex; //顶点域
	EdgeNode *firstedeg; // 边表头指针
} VertexNode;

typedef VertexNode AdjList[MaxNodeNum];

typedef struct{
	AdjList adjlist;// 邻接表
	int n,e; //顶点数和边数
}ALGraph; //对于简单应用无需定义此类型,直接使用AdjList类型。

//建立无向图的邻接表
void CreateALGraph(ALGraph *G){ 
	int i,j,k; 
	EdgeNode *s
	scanf(%d%d”, &G->n, &G->e); //读入顶点数和边数
	for(i=0; i<G->n; i++)
	{ //建立顶点表
		G->adjlist[i].vertex = getchar( ); //读入顶点信息
		G->adjlist[i].firstedge = NULL;
	} //边表置空
	
	for(k=0; k<G->e; k++)
	{ //建立边表
		scanf(%d%d”, &i, &j); //读入边(vi,vj)的顶点对序
		s = (EdgeNode *)malloc(sizeof(EdgeNode)); //生成边表结点
		s->adjvex = j; //邻结点的序号为j
		s->next = G-> adjlist[i].firstedge;//前插入
		G-> adjlist[i].firstedge = s; //将新结点*s插入vi头部
		
		s= (EdgeNode *)malloc(sizeof(EdgeNode));
		s->adjvex = i; //邻接序号为i
		s->next = G-> adjlist[j].firstedge;
		G-> adjlist[j].firstedge = s; //将新结点*s插入vj头部
	}
}

3. 图的邻接矩阵与邻接表的比较

  • 一个图的邻接矩阵表示是唯一的;邻接表表示不唯一。
    邻接表中各边表结点的次序取决于建立算法和及输入边的次序。
  • 邻接表(逆邻接表)中,每个边表对应邻接矩阵中的一行(或一列);边表中结点的个数等于邻接矩阵中的一行(或一列)非0元素的个数。
  • 邻接表或逆邻接表的空间复杂度为S(n,e)=O(n+e)。若图中的边数e远远小于n2,称为稀疏图,其邻接表比邻接矩阵要节省存储空间。当边数e接近n2 (无向图:e接近n(n-1)/2);有向图:e接近 n(n-1)时,称为稠密图,考虑链域占空间,应选择邻接矩阵存储为宜。
  • 求有向图顶点的度,采用邻接矩阵比邻接表结构方便。 在邻接表(出边表)结构中,求顶点的出度容易,入度困难;逆邻接表(入边表)中, 求顶点的入度容易,出度困难。
  • 判断边,邻接矩阵比邻接表容易;求边数:邻接矩阵中花费的时间复杂度为O(n2),邻接表中花费的时间复杂度为O(n+e)。

4. 十字链表

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

答案是肯定的,就是把它们整合在一起。这种存储有向图的方法是:十字链表(Orthogonal List).

我们重新定义顶点结点结构为:

DataFirstInFirstOut
数据域入边表头指针出边表头指针
  • FirstIn 表示入边表头指针,指向该顶点的入边表中第一个结点
  • FirstOut 表示出边表头指针,指向该顶点的出边表中的第一个结点

重新定义的弧结点结构如下表:

TailVexHeadVexHeadLinkTailLink
弧起点在顶点表的下标弧终点在顶点表中的下标入边表指针域出边表指针域
  • HeadLink 是指入边表指针域,指向弧终点(弧头)相同的下一条边。
  • TailLink 是指出边表指针域,指向弧起点(弧尾)相同的下一条边。
  • 如果是网,还可以再增加一个Weight域来存储权值。

其中,下图中的横向(实线)箭头所构成的表相当于出边表,纵向(虚线)箭头则相当于入边表,所以十字链表由此得名。
在这里插入图片描述

5. 图的遍历

5.1 深度优先搜索

5.1.1 深度优先搜索法遍历图的过程

1). 访问指定的某顶点V,将V作为当前顶点
2). 访问当前顶点的下一未被访问过的邻接点,并将该顶点作为当前顶点
3). 重复2,直到当前顶点的所有邻接点都被访问过
4). 沿搜索路径回退,退到尚有邻接点未被访问过的某结点,将该结点作为当前结点,重复2,直到所有顶点被访问过的为止
举例:

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

5.1.2 递归法实现深度优先遍历算法

举例:

在这里插入图片描述

代码如下:

//从顶点v0出发深度优先遍历g中能访问的各个顶点
void dfs(int v0)
{ 
	visited[v0]=1; /*访问标志置为 1,表示已被访问*/
	w=firstadj(g,v0); /* w是vo的第一个邻接点 */
	while (w!=0)
	{ 
		if(visited[w]==0) 
			dfs(w); /*顶点未被访问,则递归的进行深度遍历 */
		w=nextadj(g,v0,w); /*顶点已访问,则取顶点v0在w后面的下一个邻接点 */
	}
}

5.2 广度优先搜索

5.2.1 广度优先搜索过程

举例:

在这里插入图片描述

1). 首先访问指定顶点v1,将v1作为当前顶点;
2). 访问当前顶点的所有未访问过的邻接点,并依次将访问的这些邻接点作为当前顶点;
3). 重复2, 直到所有顶点被访问为止。

5.2.2 具体算法

对每一个当前访问顶点,一次性访问其所有的邻接点,采用队列来实现。

Void bfs(Graph g,int v0) { /* 从v0出发广度优先遍历图g */
	visited[v0]=1;
	Enqueue(Q,v0);
	While (!Empty(Q)){
		v=Dlqueue(Q); /* 取队头元素v */
		w=Firstadj(g,v); /* 求v的第一个邻接点 */
		while(w!=0){
			if(visited[w]==0){
				visited[w]=1;
				Enqueue(Q,w);
			}
			w=Nextadj(g,v,w); /* 求下一个邻接点 */
		}
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值