数据结构-图

目录

一、图的性质

1、易错术语

2、图的性质

二、图的存储

1、邻接矩阵法

1.1、存储结构

1.2、特点

2、邻接表法

2.1、存储结构

2.2、特点

3、邻接矩阵和邻接表适用性差异

4、十字链表

5、邻接多重表

6、图的存储总结

7、图的基本操作

三、图的遍历

1、广度优先搜索

1.1、思想

1.2、邻接表实现-代码

1.3、邻接矩阵实现-代码

1.4、BFS求单源最短路径-非带权图

1.5、BFS求广度优先生成树

2、深度优先搜索

2.1、思想

2.2、邻接表实现-代码

2.3、邻接矩阵实现-代码

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

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

四、图的应用

1、最小生成树

1.1、定义及性质

1.2、Prim算法

1.3、Kruskal算法

2、最短路径

2.1、Dijkstra算法

2.2、Floyd算法

2.3 总结

3、有向无环图描述表达式

4、拓扑排序

4.1、性质及代码

4.2、DFS实现拓扑排序

5、关键路径

5.1、相关定义

5.2、求关键路径的步骤

6、采用不同存储结构时各种图算法的时间复杂度


一、图的性质

1、易错术语

1.1  <v,w>,v到w的弧,v为弧尾,w为弧头

1.2  简单图:不存在重复的边,不存在顶点到自身的边

1.3  完全图:无向:任意两个顶点之间都存在边,有n(n-1)/2条边;有向:任意两个顶点间存在方向相反的弧,有n(n-1)条弧

1.4  生成子图:A是B的子图,且B的顶点都在A中,边可以不全在

1.5  连通图:任意两个顶点都是连通的

1.6  连通分量:无向图中的极大连通子图(极大连通子图:子图连通并包含尽可能多的边)

1.7  强连通图:任意一对顶点都是强连通的(正向+逆向都有)

1.8  极大强连通分量:有向图的极大连通子图

1.9  生成树:包含全部顶点的极小连通子图

1.10  顶点间的路径:顶点序列

1.11  路径长度:路径上的边的数目

1.12  简单路径:在路径序列中,顶点不重复出现的路径

1.13  简单回路:除首尾顶点,其余顶点不重复

1.14  有向树:一个顶点的入度为0,其余顶点的入度为1的有向图

2、图的性质

2.1 若顶点数为n,则生成树含n-1条边

2.2  边数总结

n为顶点minmax
无向图非连通0C_{n-1}^{2}(n-1个顶点的完全图+一个顶点)
无向图连通n-1(一个图有n个顶点,若边数小于n-1,则此图必是非连通图)C_{n}^{2}
非强连通0(n-2)(n-1)+1 (n-1个顶点的强连通图+一条边+一个顶点)
强连通n(环)C_{n}^{2}*2

二、图的存储

1、邻接矩阵法

1.1、存储结构

#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
	VertexType vex[MaxVertexNum];  //顶点表
	EdgeType edge[MaxVertexNum][MaxVertexNum];  //邻接矩阵,边表
	int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;

空间复杂度为O(n^2),n为顶点数

1.2、特点

①无向图的邻接矩阵一定是个对称矩阵,在实际存储邻接矩阵时只需要存储上/下三角矩阵的元素

②对于无向图,第i行/列非零元素个数是顶点i的度

③对于有向图,第i行非零元素个数代表出度,第i列非零元素的个数是入度

④稠密图适合采用邻接矩阵的存储表示

⑤A^n的元素A^n[i][j]等于由顶点i到顶点j的长度为n的路径的数目

2、邻接表法

2.1、存储结构

#define MaxVertexNum 100  //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;

//边表结点
typedef struct ArcNode {
	int adjvex;  //弧所指向的顶点的编号
	struct ArcNode* nextarc;
}ArcNode;

//顶点表结点
typedef struct VNode {
	VertexType data;  //顶点信息
	ArcNode* firstarc;  //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];

//类型
typedef struct {
	AdjList vertices; //邻接表
	int vexnum, arcnum;
}ALGraph;

2.2、特点

①G若为无向图,所需的存储空间为O(V+2E),若G为有向图,存储空间为O(V+E)。

3、邻接矩阵和邻接表适用性差异

①对于稀疏图,采用邻接表表示极大节省了空间

②给定顶点,查找所有邻边:邻接表很方便,读取它的邻接表即可,但邻矩阵花费时间为O(n)

③确定给定两个顶点是否存在边:邻接矩阵立刻可查到,但邻接表中需要在相应结点对应的边表中查找另一结点

④求度:无向图邻接表中,边表结点的个数

⑤求出度:在有向图邻接表中,边表结点个数

⑥求入度:遍历全部邻接表,统计邻接点(adjvex)域为x的边表结点个数

4、十字链表

有向图的链式存储结构

弧结点顶点结点
tailvexheadvexhlinktlinkinfodatafirstinfirstout
弧尾顶点编号弧头编号弧头相同的下一条弧弧尾相同的下一条弧该顶点作为弧头的第一条弧该顶点作为弧尾的第一条弧

图的十字链表是不唯一的,但一个十字链表表示唯一确定一个图

5、邻接多重表

无向图的链式存储结构

弧结点顶点结点
ivexilinkjvexjlinkinfodatafirstedge
该边依附的顶点编号指向下一条依附于ivex的边该边依附的顶点编号指向下一条依附于jvex的边第一条依附于该顶点的边

6、图的存储总结

邻接矩阵邻接表十字链表邻接多重表
空间复杂度O(V^2),V为顶点数无向图:O(V+2E),有向图:O(V+E)O(V+E)O(V+E)
找相邻边遍历行/列找有向图的入度遍历整个邻接表方便方便
删除边或顶点

边-方便

顶点-移动大量数据

都不方便方便方便
适用于稠密图稀疏图+其他只能存有向图只能存有向图
表示方式唯一不唯一不唯一不唯一

7、图的基本操作

三、图的遍历

注:所有顶点访问一次且仅访问一次

1、广度优先搜索

1.1、思想

        类似于二叉树层次遍历,因此要借助辅助队列。同Dijkstra单源最短路径算法和Prim最小生成树算法。对于邻接矩阵得到的序列是唯一的,对于邻接表得到的序列不唯一

1.2、邻接表实现-代码

#define MaxVertexNum 100  //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;

typedef struct {
	VertexType vex[MaxVertexNum];  //顶点表
	EdgeType edge[MaxVertexNum][MaxVertexNum];  //邻接矩阵,边表
	int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;

//边表结点
typedef struct ArcNode {
	int adjvex;  //弧所指向的顶点的编号
	struct ArcNode* nextarc;
}ArcNode;

typedef struct VNode {
	VertexType data;  //顶点信息
	ArcNode* firstarc;  //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];

//类型
typedef struct {
	AdjList vertices; //邻接表
	int vexnum, arcnum;
}ALGraph;

bool visited[MaxVertexNum];
void BFSTraverse(ALGraph G) {
	for (int i = 0; i < G.vexnum; i++)   //初始化标记数组
		visited[i] = false;
	InitQueue(Q);
	for (int i = 0; i < G.vexnum; i++) {
		if (!visited[i])   //如果该结点没有被访问过,进行访问
			BFS(G, i);
	}
}

void BFS(ALGraph G, int i) {
	visite(i); 
	visited[i] = true;
	Enqueue(Q, i);
	while (!isEmpty(Q)) {
		DeQueue(Q,v);//队首顶点v出队
		ArcNode* p;
		for (p = G.vertices[i].firstarc; p; p = p->nextarc) { //检测v的所有邻接点
			int w = p->adjvex; // 弧所指向的顶点的编号
			if (visited[w] == false) {  //没被访问过
				visit(w);
				visited[w] = true;
				EnQueue(Q, w);
			}
		}
	}
}

每个顶点需入队一次,时间复杂度为O(V),在搜索每个顶点的邻接表时,每条边至少访问一次,时间复杂度为O(E),总为O(V+E)   空间复杂度:O(V)

1.3、邻接矩阵实现-代码

#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
	VertexType vex[MaxVertexNum];  //顶点表
	EdgeType edge[MaxVertexNum][MaxVertexNum];  //邻接矩阵,边表
	int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;

bool visited[MaxVertexNum];
void BFSTraverse(MGraph G) {
	for (int i = 0; i < G.vexnum; i++)
		visited[i] = false;
	InitQueue(Q);
	for (int i = 0; i < G.vexnum; i++) 
		if (!visited[i])
			BFS(G, i);
}
void BFS(MGraph G, int i) {
	visit(i);
	visited[i] = true;
	Enqueue(Q,i);
	while (!isEmpty(Q)) {
		Dequeue(Q, v);
		for (int w = 0; w < G.vexnum; w++) {  //遍历v的所有顶点
			if (visited[w] == false && G.edge[v][w] == 1) {   //w没被访问过且v,w之间存在边 
				visit(w);
				visited[w] = true;
				Enqueue(Q, w);
			}
		}
	}
}

查找每个顶点的邻接点所需的时间为O(V),总为O(V+V^2)  空间复杂度:O(V)

1.4、BFS求单源最短路径-非带权图

void BFS_MIN_DISTANCE(Graph G, int u) { 
	for (int i = 0; i < G.vexnum; i++)
		d[i] = -999;  //d[i]表示u到i结点的最短路径
	visited[u] = true;
	d[u] = 0;
	Enqueue(Q, u);
	while (!isEmpty(Q)) {
		DeQueue(Q,u);//队首顶点v出队
		for (w = G.FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)) { //检测v的所有邻接点
			if (!visited[w]) {  //没被访问过
				visited[w] = true;
				d[w] = d[u] + 1;
				EnQueue(Q, w);
			}
		}
	}
}

1.5、BFS求广度优先生成树

注意:邻接矩阵遍历所生成的生成树是唯一的,但邻接表所生成的生成树不唯一

2、深度优先搜索

2.1、思想

        类似于树的先序遍历;借助栈,若在遍历过程中欲访问结点已在栈中,则有环。对于邻接矩阵得到的序列是唯一的,对于邻接表得到的序列不唯一

2.2、邻接表实现-代码

#define MaxVertexNum 100  //顶点数目最大值
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;

typedef struct {
	VertexType vex[MaxVertexNum];  //顶点表
	EdgeType edge[MaxVertexNum][MaxVertexNum];  //邻接矩阵,边表
	int vexnum, arcnum; //图的当前顶点数和边数
}MGraph;

//边表结点
typedef struct ArcNode {
	int adjvex;  //弧所指向的顶点的编号
	struct ArcNode* nextarc;
}ArcNode;

typedef struct VNode {
	VertexType data;  //顶点信息
	ArcNode* firstarc;  //指向第一条依附该顶点的弧的信息
}VNode,AdjList[MaxVertexNum];

//类型
typedef struct {
	AdjList vertices; //邻接表
	int vexnum, arcnum;
}ALGraph;

bool visited[MaxVertexNum];
void DFSTraverse(ALGraph G) {
	for (int i = 0; i < G.vexnum; i++)
		visited[i] = false;
	for (int i = 0; i < G.vexnum; i++)
		if (!visited[i])
			DFS(G, i);
}
void DFS(ALGraph G, int i) {
	visit(i);
	visited[i] = true;
	ArcNode* p;
	for (p = G.vertices[i].firstarc; p; p = p->nextarc) { //检测v的所有邻接点
		int w = p->adjvex; // 弧所指向的顶点的编号
		if (visited[w] == false) {  //没被访问过
			DFS(G, w);
		}
	}
}

时间复杂度:O(V+E)   空间复杂度:O(V)

2.3、邻接矩阵实现-代码

#define MaxVertexNum 100
typedef char VertexType;
#define INFINITY //带权
typedef int EdgeType;
typedef struct {
	VertexType vex[MaxVertexNum];  //顶点表
	EdgeType edge[MaxVertexNum][MaxVertexNum];  //邻接矩阵,边表
	int vexnum, arcnum; //图的当前顶点数和边数

bool visited[MaxVertexNum];
void DFSTraverse(MGraph G) {
	for (int i = 0; i < G.vexnum; i++)
		visited[i] = false;
	for (int i = 0; i < G.vexnum; i++) 
		if (!visited[i])
			DFS(G, i);
}
void DFS(MGraph G, int i) {
	visit(i);
	visited[i] = true;
	for (int j = 0; j < G.vexnum; j++) {
		if (visited[i] == false && G.edge[i][j] == 1)
			DFS(G, j);
	}
}

时间复杂度:O(V^2)   空间复杂度:O(V)

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

对于连通图调用DFS才能产生深度优先生成树,否则是优先生成森林

邻接矩阵遍历所生成的生成树是唯一的,但邻接表所生成的生成树不唯一

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

图的遍历算法可以判断图的连通性

在BFSTraverse和DFSTraverse中,对于无向图,调用B/DFS的次数等于该图的连通分量数。

但对于有向图不然,一个连通的有向图分别为强连通和非强连通,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用B/DFS无法访问该连通分量的所有结点

但并非任意非强连通图必须2次及以上遍历才可以访问所有结点,如①->②->③

四、图的应用

1、最小生成树

1.1、定义及性质

①最小生成树:权值之和最小的生成树(一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边)

②若无向连通图的边数比顶点数少1,那么它本身就是一棵最小生成树

③最小生成树中所有边的权值之和最小,但不能确保任意两个顶点之间的路径是最短路径

④贪心算法-构造最小生成树

GENERIC_MTS(G){
    T=NULL;
    while(T未形成一棵生成树){
            找到一条最小代价边(u,v)并加入T后不产生回路;
            T=T∪(u,v);
    }

1.2、Prim算法

类似于找最短路径

void Prim(G,T){
    T=空;
    U={w};  //任意加一个顶点w
    while((V-U)!=空){  //当所有顶点都不全在U中
        T=T∪{(u,v)};   //{u,v}是当前集合顶点的权值最小的边
        U=U∪{v};
}

时间复杂度:O(V^2) ,适用于求解边稠密的图的最小生成树

1.3、Kruskal算法

void Kruskal(G,T){
    T=V;   //仅含顶点
    numS=n;  //连通分量数
    while((numS>1){ 
        从边集合中取出权值最小的边(v,u)
        if(v,u属于T中不同的连通分量)
        T=T∪{(u,v)};   //加入生成树
        numS--;
}

时间复杂度:O(Elog_2{E})  ,适合于边稀疏而顶点较多的图

2、最短路径

2.1、Dijkstra算法

①求图中某一顶点到其他各顶点的最短路径

②在集合S中加入源点v,每次加入一个新顶点,同时修改v到V-S中顶点的最短路径长度

③邻接矩阵表示时,时间复杂度:O(V^2)

④Prim和Dijkstral算法的差异:

                目的不同:D目的是构建单源点的最短路径树,P是构建最小生成树

                算法思路稍有不同:P是从一个点开始,每次选择权值最小的边,将其连接到已构建的生成树上,直至所有顶点都已加入;D是每次找出到源点距离最近且未归入集合的点,并把它归入集合,同时以这个点为基础更新从源点到其他所有顶点的距离。

                适用图不一样:P适用于带权无向图;D适用于带权有向图或带权无向图

                时间复杂度不同

⑤边上带有负权值时,Dijkstra算法不适用

2.2、Floyd算法

①求图中所有顶点之间的最短路径

②先初始化一个矩阵,内容为每个顶点之间的直接路径长度,经过递推产生后序矩阵,如矩阵A0表示以v0结点作为中间顶点,所更新得到的矩阵,依次类推

③时间复杂度:O(V^3)  空间复杂度:O(V^2)

④Floyd算法允许图中带有负权值的边,但不允许有包含带负权值的边组成的贿赂

⑤适用于带权无向图

2.3 总结

BFSDijkstraFloyd
用途求单源最短路径求单源最短路径求各顶点之间的最短路径
无权图适用适用适用
带权图不适用适用适用
带负权值的图不适用不适用适用
带负权值回路的图不适用不适用不适用
时间复杂度O(V^2)或O(V+E)O(V^2)O(V^3)

3、有向无环图描述表达式

描述含有公共子式的工具

4、拓扑排序

4.1、性质及代码

①AOV网:有向无环图表示工程,顶点表示活动,有向边表示关系

②拓扑排序:对有向无环图的顶点的排序,使得若存在一条从顶点A到顶点B的路径,则在排序中B出现在A的后面,每个AOV网都有一个或多个拓扑排序序列

③若网中不存在无前驱的顶点,说明有向图中存在环

bool TopologicalSort(ALGraph G) {
	InitStack(S);
	int i;
	for (i = 0; i < G.vexnum; i++)
		if (indegree[i] == 0)   //将入度为0的顶点入栈
			Push(S, i);
	int count = 0;  //已输出的顶点数
	while (!isEmpty(S)) {
		Pop(S, i);
		print[count++] = i;  //输出顶点
		ArcNode* p;
		for (p = G.vertices[i].firstarc; p; p = p->nextarc) {
			//将所有i指向的顶点的入度都-1,并将入度减为0的顶点压入栈
			int v = p->adjvex;//p的下一个顶点
			if (!(--indegree[v]))
				Push(S, v);
		}
	}
	if (count < G.vexnum)
		return false;
	else return true;
}

邻接表存储。时间复杂度:O(V+E)

邻接矩阵存储,时间复杂度:O(V^2)

4.2、DFS实现拓扑排序

bool visited[MaxVertexNum];
void DFSTraverse(ALGraph G) {
	for (int i = 0; i < G.vexnum; i++)
		visited[i] = false;
	int time = 0;
	for (int v = 0; v < G.vexnum; v++)
		if (!visited[v])
			DFS(G, v);
}
void DFS(ALGraph G, int v) {
	visited[v] = true;
	visit(v);
	for(w = G.FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) { //检测v的所有邻接点
		if (!visited[w])
			DFS(G, w);
		time = time + 1;
		finishTime[v] = time;
}

5、关键路径

5.1、相关定义

①AOE网:对带权有向图,顶点表示事件,有向边表示活动,权值表示开销

②只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始

③只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生

④关键路径:最大路径长度的路径

⑤关键活动:关键路径上的活动

5.2、求关键路径的步骤

①求出事件Vk的最早发生时间Ve(k)  -从源点V1到顶点Vk的最长路径长度

                Ve(源点)=0;     Ve(k)=max{前一个顶点的最长路径长度+两个顶点之间的权值}

②求出事件Vk的最迟发生时间Vi(k)  

                Vi(汇点)=Ve(汇点);     Vi(k)=min{后继顶点的最迟发生时间-两个顶点之间的权值}

③活动ai最早开始时间e(i)  -该活动弧的起点所表示的事件的最早发生事件

                e(i)=Ve(k);     

④活动ai的最迟开始时间 l(i)  -活动弧的终点所表示事件的最迟发生事件与该活动所需时间之差

⑤一个活动ai的最迟开始时间和最早开始时间的差 -活动完成的时间余量

                当余量=0时,活动ai为关键活动

5.3、缩短工期

①不能任意缩短关键活动

②关键路径不唯一,对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包含在所有关键路径上的关键活动才能缩短工期

6、采用不同存储结构时各种图算法的时间复杂度

DijkstraFloydPrimKruskalDFSBFS拓扑排序关键路径
邻接矩阵O(n^2)O(n^3)O(n^2)O(n^2)O(n^2)O(n^2)O(n^2)
邻接表O(eloge)O(n+e)O(n+e)O(n+e)O(n+e)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值