数据结构-图

图的概念

图的定义

图G是由顶点集合V(G)和边集合E(G)构成。

如果代表边的顶点对是无序的,则称G为无向图,用圆括号序偶表示无向边。
有序图用尖括号表示。

图的基本术语

1、端点和邻接点
无向图:若存在一条边(i,j),顶点i、j为端点,它们互为邻接点
有向图:存在一条边<i,j>,顶点i为起始端点,j为终止端点,它们互为邻接点。

2、顶点的度、入度和出度
无向图:以顶点i为端点的边数称为该顶点的度。
有向图:以顶点i为终点的入边的数目称为该顶点的入度。以顶点i为始点的出边的数目称为该顶点的出度。一个顶点的入度和出度和称为该顶点的度。

3、完全图
无向图:每两个顶点之间都存在一条边,称为完全无向图,包含有n(n-1)/2条边
有向图:每两个顶点间存在方向相反的两条边。包含有n(n-1)条边

4、稠密图、稀疏图

5、子图
设有两个图G=(V,E)和G’=(V’,E’),若V’是V的子集,E’是E的子集,则G’是G的子图。

6、路径和路径长度
路径长度是指一条路径上经过边的数目。
若一条路径上除开始点和结束点可以相同外,其余顶点均不相同,则称此路径为简单路径。

7、回路和环
若一条路径上的开始点与结束点为同一个顶点,即为回路或环。
开始点与结束点相同的简单路径被称为简单回路或简单环。

8、连通、连通图和连通分量
无向图:若从顶点i到顶点j有路径,则称顶点i和j是连通。
连通图:若图中任意两个顶点都连通。
连通分量:无向图G中的极大连通子图。任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。

9、强连通图和强连通分量
有向图:若从顶点i到顶点j有路径,则称从顶点i到j是连通的
若图G中任意两个顶点i和j都连通,即从顶点i到j和从顶点j到i都存在路径,则图G是强连通图。
强连通分量:有向图G中极大强连通子图。强连通图只有一个强连通分量,即本身。非强连通图可能有多个。

在一个非强连通中找强连通分量:
1、在图中找有向环
2、扩展该有向环:如果某个顶点到该环中任一顶点有路径,并且该环中任一顶点到这个顶点也有路径,则加入这个顶点。

10、权和网
边上有权的图称为带权图,也称作网。

图的存储结构

邻接矩阵

邻接矩阵是表示顶点之间相邻关系的矩阵。设G=(V,E)是具有n个顶点的图。
G的邻接矩阵A是n阶方阵:
若G是无向图,则:A[i][j]=1:若(i,j)属于E(G)
若G是有向图,则:A[i][j]=1:若<i,j>属于E(G)
若G是带权无向图,则:A[i][j]=wij:若i不等于j且(i,j)属于E(G) 0:i=j 无穷:其他
若G是带权有向图,则:A[i][j]=wij:若i不等于j且<i,j>属于E(G) 0:i=j 无穷:其他
主要特点:
1、一个图的邻接矩阵表示唯一
2、特别适合稠密图的存储
存储空间为O(n^2)

图的邻接矩阵存储类型如下:

#define MAXV <最大顶点个数>
typedef struct
{	int no;//顶点编号
    InfoType info;
}VertexType;

typedef struct
{	int edges[MAXV][MAXV];
	int n,e;//顶点数,边数
	VertexType vexs[MAXV];//存放顶点信息
}MGraph;
邻接表存储方法

1、对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
2、每个单链表上添加一个表头节点。并将所有表头节点构成一个数组,下标为i的元素表示顶点i的表头节点。

特点:
1、邻接表表示不唯一
2、特别适合于稀疏图存储。邻接表的存储空间为O(n+e)

图的邻接表存储类型定义如下:

typedef struct ANode
{	int adjvex;//该边的终点编号
	struct ANode *nextarc;//指向下一条边的指针
	InfoType info;
}ArcNode;

typedef struct VNode
{	Vertex data;
	ArcNode *firstarc;//指向第一条边
}VNode;

typedef struct
{	VNode adjlist[MAXV];//邻接表
	int n,e;
}ALGraph;

图的遍历

深度优先遍历

算法设计思路:深度优先遍历体现后进先出:栈或递归。
邻接表DFS算法:

void DFS(ALGraph *G,int v)
{	ArcNode *p,int w;
	visited[v]=1;
	printf("%d ",v);//输出被访问顶点的编号
	p=G->adjlist[v].firstarc;
	while(p!=NULL)
	{	w=p->adjvex;
		if(visited[w]!=1)
			DFS(G,w);
		p=p->nextarc;
	}
}

该算法复杂度为O(n+e)。
距离初始顶点越远越优先访问。

广度优先遍历

广度优先搜索遍历体现先进先出特点,用队列实现。

void BFS(ALGraph *G,int v)
{	ArcNode *p,int w,i;
	int queue[MAXV],front=0,rear=0;//定义循环队列
	int visited[MAXV];
	for(i=0;i<G->n;i++)
		visited[i]=0;
	printf("%2d",v);
	visited[v]=1;
	rear=(rear+1)%MAXV;
	queue[rear]=v;//v进队
	while(front!=rear)
	{	front=(front+1)%MAXV;
		w=queue[front];//出队
		p=G->adjlist[w].firstarc;
		while(p!=NULL)
		{	if(visited[p->adjvex]==0)
			{	printf("%2d",p->adjvex);
				visited[p->adjvex]=1;
				rear=(rear+1)%MAXV;
				queue[rear]=p->adjvex;
			}
			p=p->nextarc;//找下一个邻接顶点
		}
	}
}

距离初始点越近越优先访问。

非连通图的遍历

无向非连通图调用一次DFS或BFS只能访问到初始点所在连通分量中所有顶点,不可能访问到其他连通分量重的顶点。只要分别遍历每个连通分量,才能够访问到所有顶点。

void DFS1(ALGraph *G)
{	int i;
	for(i=0;i<G->n;i++)
		if(visited[i]==0)
			BFS(G,i);
}

调用BFS次数恰好等于连通分量个数

生成树和最小生成树

生成树概念:一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的n-1条边。
可通过遍历方法得到生成树。

最小生成树概念:对于带权连通图G,权值之和最小的生成树称为图的最小生成树。

连通图:仅需调用遍历过程一次,从图中任一顶点出发,便可以遍历图中各顶点。
非连通图:需多次调用遍历过程。

Prim算法

构造性算法,构造最小生成树。
思路:
1、初始化U={v},v到其他顶点的所有边为侯选边
2、重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
(1)从侯选边中挑选权值最小的边输出,设该边在V-U中顶点是k,将k加入U中
(2)考察当前V-U中所有顶点j,修改侯选边:若(j,k)的权值小于原来和顶点k关联的侯选边,则用(k,j)取代后者作为侯选边。

Prim算法如下:

#define INF 32767 //INF表示无穷
void Prim(MGraph g,int v)
{	int lowcost[MAXV];
	int min;
	int closest[MAXV],i,j,k;
	for(i=0;i<g.n;i++)
	{	lowcost[i]=g.edges[v][i];
		closest[i]=v;
	}
	for(i=1;i<g.n;i++)//输出n-1条边
	{	min=INF;
		for(j=0;j<g.n;j++) //在V-U中找出离U最近的顶点k
			if(lowcost[j]!=0&&lowcost[j]<min)
			{	min=lowcost[j];
				k=j;//k记录最近顶点编号
			}
		printf("边(%d,%d)权为:%d\n",closest[k],k,min);
		lowcost[k]=0;//标记k已经加入U
		for(j=0;j<g.n;j++)//修改数组lowcost和closest
			if(lowcost[j]!=0&&g.edges[k][j]<lowcost[j])
			{	lowcost[j]=g.edges[k][j];
				closest[j]=k;
			}
		}
	}
	

局部最优+调整=全局最优
有两重for循环所以时间复杂度为O(n^2)

Kruskal算法

求带权无向图的最小生成树的构造性算法,按权值的递增次序选择合适的边来构造最小生成树。
算法过程:
1、置U的初值等于V,TE的初值为空寂
2、将G中边按权值从小到大顺序依次选取:
若选取的边未使生成树T形成回路,则加入TE
否则舍弃,直到TE中包含n-1条边为止

用数组E存放图G中所有边,类型如下:

typedef struct
{	int u;//边的起始顶点
	int v;//边的终止顶点
	int w;//权值
}Edge;

算法如下:

void Kruskal(MGraph g)
{	int i,j,u1,v1,sn1,sn2,k;
	int vset[MAXV];
	Edge E[MaxSize];//存放所有边
	k=0;
	for(i=0;i<g.n;i++)
		for(j=0;j<g.n;j++)
		    if(g.edges[i][j]!=0&&g.edges[i][j]!=INF)
		    {	E[k].u=i;E[k].v=j;E[k].w=g.edges[i][j];
		    	k++;
		    }
	InsertSort(E,g.e);//直接插入排序对E数组按权值递增排序
	for(i=0;i<g.n;i++)//初始化辅助数组
		vset[i]=i;
	k=1;j=0;
	while(k<g.n)
	{	u1=E[j].u;v1=E[j].v;
		sn1=vset[u1];sn2=vset[v1];
		if(sn1!=sn2)
		{	printf("(%d,%d):%d\n",u1,v1,E[j].w);
			k++;
			for(i=0;i<g.n;i++)
				if(vset[i]==sn2)//集合编号为sn2的改为sn1
		}
		j++;//扫描下一条边
	}
}

上述不是最优,改进:堆排序、并查集
时间复杂度为O(elog2e)

最短路径

Dijkstra算法

设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组:
1、已求出最短路径顶点集合
2、为求出最短路径顶点集合

void Dijkstra(MGraph g,int v)
{	int dist[MAXV],path[MAXV];
	int s[MAXV];
	int mindis,i,j,u;
	for(i=0;i<g.n;i++)
	{	dist[i]=g.edges[v][i];//距离初始化
		s[i]=0;
		if(g.edges[v][i]<INF)
			path[i]=v;
		else path[i]=-1;
	}
	s[v]=1;
	for(i=0;i<g.n;i++)
{	mindis=INF;
	for(j=0;j<g.n;j++)
		if(s[j]==0&&dist[j]<mindis)
		{	u=j;
			mindis=dist[j];
		}
		s[u]=1;//顶点u加入S中
		for(j=0;j<g.n;j++)//修改不在s中的顶点的距离
		if(s[j]==0)
		if(s[j]==0)
			if(g.edges[u][j]<INF&&dist[u]+g.edges[u][j]<dist[j])
			{ dist[j]=dist[u]+g.edges[u][j];
				path[j]=u;
			}
	}
	Dispath(dist.path,s,g.n,v);
}

时间复杂度:O(n^2)
特点:1、按顶点进入S的先后顺序,最短路径长度越来越长
2、一个顶点一旦进入S后,其最短路径长度不再改变

Floyd算法

迭代思路:采用邻接矩阵存储。设置一个二维数组A用于存放当前顶点之间最短路径长度。分量A[i][j]表示当前顶点i->j的最短路径长度。
初始时,A-1[i][j]=g.edges[i][j]
考虑从i->j的最短路径经过编号为k顶点的情况:
Ak[i,j]=MIN{Ak-1[i,j],Ak-1[i,k]+Ak-1[k,j]}

用二维数组A存储最短路径长度:
Ak[i][j]表示考虑顶点0-k后得出的i->j的最短路径长度。
An-1[i][j]表示最终的i->j的最短路径长度。

用二维数组path存放最短路径:
pathk[i][j]表示考虑顶点0-k后得出的i->j的最短路径
pathn-1[i][j]表示最终i->j的最短路径

算法如下:

void Floyd(MatGraph g)//求每对顶点之间的最短路径
{	int A[MAXVEX][MAXVEX];
	int path[MAXVEX][MAXVEX];
	int i,j,k;
	for(i=0;i<g.n;i++)
		for(j=0;j<g.n;j++)
		{	A[i][j]=g.edges[i][j];
			if(i!=j&&g.edges[i][j]<INF)
			path[i][j]=i;//i和j顶点之间有一条边时
			else path[i][j]=-1;
		}
		for(k=0;k<g.n;k++)//求Ak[i][j]
		{	for(i=0;i<g.n;i++)
				for(j=0;j<g.n;j++)
					if(A[i][j]>A[i][k]+A[k][j])
					{	A[i][j]=A[i][k]+A[k][j];
						path[i][j]=path[k][j];
					}
		}
}

拓扑排序

步骤:
1、从有向图中选择一个没有前驱的顶点并且输出
2、从图中删去该顶点,并且删去从该顶点发出的全部有向边
3、重复上述两步,直到剩余的图中不再存在没有前驱的顶点

拓扑排序算法设计:
邻接表定义中VNode类型修改如下:

typedef struct
{	Vertex data;//顶点信息
	int count;//存放顶点入度,用于查找入度为0的顶点
	ArcNode *firstarc;//指向第一条边
}VNode;
void TopSort(VNode adj[],int n)
{	int i,j;int St[MAXV],top=-1;
	ArcNode *p;
	for(i=0;i<n;i++)
		if(adj[i].count==0)
		{top++;St[top]=i;}
		while(top>-1)
		{	i=St[top];top--;
		printf("%d",i)
		p=adj[i].firstarc;
		while(p!=NULL)
		{	j=p->adjvex;
		adj[j].count--;
		if(adj[j].count==0)
		{top++;St[top]=j;}
		p=p->nextarc;
		}
	}
}		
	```

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值