数据结构-图

326 阅读17分钟

一、基本概念

  1. 图:G(V,E),V(G)是顶点集合,E(G)是边集

  2. 有向图:边又叫弧,是顶点的有序对<v,w>,v头w尾(尾巴带箭头)

  3. 无向图:(v,w)=(w,v)

  4. 本章只考虑没有重复边,没有环的图

  5. 网:边带权的无向图——无向网;弧带权的有向图——有向网

  6. 子图:设图G=(V,E) 和图 G′=(V′,E′),且V′⊆V, E′⊆E,则称 G′ 为 G 的子图

  7. 完全图:n 个顶点的含有 n(n-1)/2 条边的无向图称作完全图; n 个顶点的含有 e=n(n-1) 条弧的有向图称作有向完全图

  8. 若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图
    1713610241772.png

  9. 邻接点、关联:假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点,边(v,w) 和顶点v 和w 相关联

  10. 度:无向图中和顶点v 关联的边的数目定义为v的度,记为TD(v)

  11. 有向图的度:入度ID(v)——被指向,出度OD(v)——指出去,度(TD)=出度+入度

  12. 路径长度:无向(有向)图从顶点u 到顶点w 之间存在一条(有向)路径,路径上边的数目称作路径长度

  13. 简单路径:序列中顶点不重复出现的路径

  14. 简单回路:序列中第一个顶点和最后一个顶点相同的路径

  15. 连通图:若无向图G中任意两个顶点之间都有路径相通,则称此图为连通图(从任一点出发均能访问所有点)

  16. 连通分量:若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量

  17. 强连通图:有向图中若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。否则,其各个极大强连通子图称作它的强连通分量(从任一点出发均能访问所有点)

1713611004823.png 18. 生成树:假设一个连通图有 n 个顶点和 e 条边,其中 n-1条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树(生成树不唯一) 19. 生成森林:对非连通图,则称由各个连通分量的生成树的集合为此非连通图的生成森林

1713611102704.png

  1. 有向树:如果一个有向图恰有1个顶点的入度为0,其余顶点入度均为1,则称该图为一棵有向树
  2. 对于非强连通图的一个强连通分量:包含其全部n个顶点、n-1条弧、且只有1个顶点的入度为0、其余的顶点入度均为1的子图称为该连通分量的有生成向树
  3. 对于非强连通图的所有强连通分量的有向生成树构成该有向图的生成森林。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧

二、图的存储表示

(一)数组(邻接矩阵)存储

1.无向图G=(V,E)

  • A[i][j]表示顶点vi 和vj 之间是否存在连边,存在为1,不存在为0。无向图邻接矩阵是对称阵,可以利用对称阵的压缩存储方法存储
  • 一维数组存放顶点信息(数据元素的值)
  • 顶点vi的度:TD(vi)=Σ(j=0...n-1)A[i][j]

2.有向图

  • A[i][j]表示是否存在顶点vi 流向顶点vj 的弧(行->列),有为1,没有为0,不一定为对称阵
  • 顶点vi的入度:ID(vi)=Σ(j=0...n-1)A[j][i]
  • 顶点vi的出度:OD(vi)=Σ(j=0...n-1)A[i][j]

3.无向网

邻接矩阵A[i][j],存在连边为wij(在顶点vi 和vj 的连边上的权值),不存在为0或无穷

4.有向网

wij表示在顶点vi 流向顶点vj 的弧上的权值,不存在边为0或无穷

#define MAXSIZE 100
typedef struct {
	int vexs[MAXSIZE];//一维数组存放顶点信息
	int arcs[MAXSIZE][MAXSIZE];//邻接矩阵
	int vexnum, arcnum;//顶点数,边数
	int kind;//图的类型,有向or无向 
}MGraph;
MGraph T; 

(二)邻接表存储(链式)

  • 对图中每个顶点建立一个单链表,为顶点vi 所建的单链表是将与顶点vi 相关联的边或弧建成一个单链表
  • 用一维数组存放每个顶点的信息和相应单链表的头指针
  • 每个数组元素存放图中一个顶点:顶点的数据(data)和为其所建单链表的头指针(firstarc)

1.无向图

  • 顶点vi 的度是第i个单链表中含有的数据元素的个数,即为vi 所建单链表的长度
  • 一条边(vi ,vj )存2次,在第i和第j个单链表(为vi 所建单链表和为vj所建单链表)中同时存在

1713615202497.png

2.有向图

1713615387557.png

1713615416542.png

1713615808740.png

typedef struct ArcNode {
	int vex;//该弧所指向的顶点的位置
	struct ArcNode *link;//指向下一条弧的指针
	InfoType *info;//该弧的相关信息的指针 
}ArcNode;//单链表节点类型

typedef struct VNode {
	int data;//顶点信息
	ArcNode *firstarc;//指向第一条依附该顶点的弧 
}VNode;//数组元素类型

typedef struct {
	VNode arc[MAXSIZE];
	int vexnum, arcnum;//顶点数,边数
	int kind;//图的类型,有向or无向 
}Graphs; 

3.网

1713615822814.png

(三)有向图的十字链表存储表示

1713616707973.png

//一条弧位于2个单链表,但只存一次
typedef struct ArcBox {//弧的结构表示 
	int tailvex, headvex;
	InfoType *info;
	struct ArcBox *hlink, *tlink;
}ArcBox;

//一维数组保存每个顶点的信息和相应2个单链表的头指针
typedef struct VexNode {//顶点的结构表示 
	int data;
	ArcNode *firstain, *firstout; 
}VexNode;


typedef struct {
	VexNode xlist[MAXSIZE];//顶点信息 
	int vexnum, arcnum;//有向图的当前顶点数和弧数  
}OLGraph; 

(四)无向图的邻接多重表存储表示

  • 每个顶点建1个单链表:与该顶点关联的边建一个单链表
  • 一条边位于2个单链表,但只存一次
  • 一维数组保存每个顶点的信息和相应单链表的头指针 1713616879542.png

1713616914247.png

typedef struct Ebox {
	int mark;
	int ivex, jvex;
	struct EBox *ilink, *jlink;
}EBox; 

三、图的遍历

(一)深度优先搜索

从图中某个未被访问的顶点v出发,访问此顶点,然后依次从v的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到
1714652300416.png 1.深度优先搜索生成树:访问时经过的顶点和边构成的子图
2.深度优先搜索生成森林:若选用多个出发点做深度优先搜索,会产生多棵深度优先搜索生成树,构成深度优先搜索生成森林
1714652393949.png 3.图的存储:邻接矩阵/邻接表
以邻接矩阵为例,图的深度优先遍历算法:

//图的邻接表存储 
typedef struct ArcNode {
	int vex;//该弧所指向的顶点的位置
	struct ArcNode *link;//指向下一条弧的指针
	InfoType *info;//该弧的相关信息的指针 
}ArcNode;//单链表节点类型

typedef struct VNode {
	int data;//顶点信息
	ArcNode *firstarc;//指向第一条依附该顶点的弧 
}VNode;//数组元素类型

typedef struct {
	VNode arc[MAXSIZE];
	int vexnum, arcnum;//顶点数,边数
	int kind;//图的类型,有向or无向 
}Graphs; 

void DFSTraverse(Graphs G) {
	//对图G作深度优先遍历
	for(v = 0; v < G.vexnum; ++v) 
		visited[v] = 0;//访问标志数组初始化,0代表未被访问
	for(v = 0; v < G.vexnum; ++v) 
		if(!visited[v]) DFS(G,v);//对未访问的顶点调用DFS 
}
void DFS(Graphs G, int v) {
	printf("%d\t", v);
	visited[v] = 1;
	p = G.arc[v].firstarc;
	while(p) {
		w = p->vex;
		if(visited[w] == 0) DFS(G, w);
		p = p->link;
	}
} 

(二)广度优先搜索

图中的某个未访问顶点v0出发,并在访问此顶点之后依次访问v0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的未访问过的邻接点,直至图中所有和v0有路径相通的顶点都被访问到。(按照与出发点v0路径长度递增的顺序访问顶点,先访问长度为1的,再2,3……)
1.广度优先搜索生成树:访问时经过的顶点和边构成的子图
2.广度优先搜索生成森林:选用多个出发点做广度优先搜索,会产生多棵广度优先搜索生成树,构成广度优先搜索生成森林

void BFSTraverse(Graphs G) {
	//对图G作广度优先遍历
	for(v = 0; v < G.vexnum; ++v) 
		visited[v] = 0;//访问标志数组初始化,0代表未被访问
	for(v = 0; v < G.vexnum; ++v) 
		if(!visited[v]) BFS(G,v);//对未访问的顶点调用BFS 
}
void BFS(Graphs G, int v) {
	int Q[MAX], f = 0, r = 0;
	printf("%d\t", v);
	visited[v] = 1;//访问v 
	Q[r++] = v;//v入队 
	while(f <r) {//当队列不空时 
		x = Q[f++]; //队头x出队 
		p = G.arc[x].firstarc;//x的邻接点p 
		while(p) {//当p存在时 
			w = p->vex;//访问x的邻接结点 
			if(visited[w] == 0) {//若w未访问过 
				visited[w] = 1;//则访问 
				printf("%d\t", w);
				Q[r++] = w;//并入队 
				p = p->link;//求下一个邻接点 
			}
		}
	} 
	
	while(p) {
		w = p->vex;
		if(visited[w] == 0) DFS(G, w);
		p = p->link;
	}
} 

ch7-3最后的代码什么意思?

四、图的连通性问题(无向图)

1.生成树的存储方式:孩子兄弟表示法、双亲表示法
2.最小生成树:带权图的生成树上的各边权值之和称为这棵树的代价。最小代价生成树是各边权值的总和最小的生成树。(不唯一)
3.最小生成树性质(MST):令G=(V, E, W)为一个带权连通图,T为G的一生成树。对任一不在T中的边uv,如果将uv加入T中会产生一回路,使得uv是回路中权值最大的边,那么树T具有MST性质。
4.Prim算法:找最小生成树
(1)流程: 任选一个顶点出发构造最小生成树,假设选v0为出发点,将v0加入当前的最小生成树T,当前T中只有一 个顶点,0条边,即T=(U,TE), U={v0},TE={}。之后重复做n-1步,每步选一个顶点 一条边加入生成树。直到不得不出现回路。
(2)图的存储:采用邻接矩阵/邻接表存放
(3) Prim算法适合于稠密图。

typedef struct{
	int adjvex;
	//当v未加入生成树,则存放v与生成树中顶点所有连边中权值最小的边 
	//closedge[v].adjvex存放的就是这条最好的边的另一个顶点 
	
	//closedge[v].lowcose存放这条最好的边的权值 
	int lowcost;//若v已加入生成树,则为0 
}EdgeType;
EdgeType closedge[MAX];

define MAX 100
define MAXEDGE 1000000
typedef struct{
 	int arcs[MAX][MAX];
 	int vexnum,arcnum;
}AGraphs;

void prim(AGraphs G,int u) {
	int i,j,k;
	EdgeType closedge[MAX];
	for(j=0;j<G.vexnum;j++) {
		closedge[j]. adjvex=u;   
		closedge[j]. lowcost=G.arcs[u][j];
	}
	closedge[u].lowcost=0;
	for(i=1;i<G.vexnum;i++) {
		k=minclosedge(closedge);
 		printf("(%d,%d)", closedge[k]. adjvex,k);
 		closedge[k]. lowcost=0;
 	for(j=0;j<G.venum;j++)
 		if(G.arcs[k][j]< closedge[j]. lowcost) {
		 	closedge[j]. lowcost= G.arcs[k][j];
 			closedge[j]. adjvex =k;
		}
	}
 }         

int minclosedge(EdgeType closedge[ ]) {
	int min,j,k;
	min=MAXEDGE;
 	k=-1;
 	for(j=0;j<G.vexnum;j++)
 		if (closedge[j].lowcost !=0&&closedge[j].lowcost<min) {
        	min=closedge[j]. lowcost;
 			k=j;
			 }
 	return k;
}      

5.Kruskal算法
(1)流程:设G=(V,E), T为G的最小生成树,初态T= (V,{})(所有顶点都在生成树中) ,按照边的权值由小到大的顺序,考察G的边集E中的各条边。若被考察的边的两个顶点属于T的两个不同的连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;若被考察边的两个顶点属于同一个连通分量(若加入,生成树就会形成回路),则舍去此边,以免造成回路,如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树。
(2) Kruscal算法适合稀疏图。

五、有向无环图(DAG图)

(一)AOV网

1.定义:每个活动用一个顶点表示,活动之间的先后制约关系用弧表示
2.拓扑有序序列:若顶点i优先于j,则在线性序列中i仍然优先于j;对于网中原来没有优先关系的顶点i与顶点j,在线性序列中也建立一个先后关系,或者i优先于j,或者j优先于i。不唯一。
3.排序方法:找当前图中入度为0的顶点(没有制约活动,或其制约活动已经安排进行)放到拓扑排序序列中,删除该顶点以及由它出发的所有边,重复步骤1、2, 直到全部顶点输出或不再存在入度为0的顶点。
若图中还有剩余顶点未被删除,说明图中有回路,不是一个AOV网
4.图的存储 1714657542510.png

#define MAXSIZE 100
typedef struct ArcNode {
	int vex; 
	struct ArcNode* link;
} ArcNode;
typedef struct VNode {
	VertexTypedata;   
	int id; //顶点的入度
	ArcNode* firstarc;} VNode;
typedef struct {
	VNodearc[MAXSIZE];
 	int vexnum,arcnum;
}Graphs;

int topsort(Graphs T) {
	int q[MAXSIZE];
	int count; //存放已经放到拓扑排序中的顶点数 
	int h=t=0;
	ArcNode * p;
 	int u,v;
 	
 	//计算所有顶点入度,将入度为0的顶点放入队列
 	for(v=0;v<T.vexnum;v++)
		T.arcs[v].id=0;
	for (v=0;v<T.vexnum;v++)
		for(p=T.arc[v].firstarc; p!=Null;p=p->link) { 
 			u=p->vex;
 			T.arc[u].id++;
 		}
 	for (v=0;v<T.vexnum;v++)
 		if (T.arc[v].id==0) q[t++]=v; //自己加判断队列是否会溢出!???
 
  	while(h!=t ) {//当队列非空时 
		v=q[h++];//将队头顶点出队 
		printf("%d",v);
		count++;//将v放到拓扑排序中 
		for(p=T.arc[v].firstarc; p!=Null;p=p->link) {//v发出的每条弧  
			u=p->vex;//弧指向的顶点u 
			T.arc[u].id--;//将u对入度减1 
		}
 		if (T.arc[u].id==0)//若u得入度变为0 
		 	q[t++]=u;//则把u放入队尾
			 
			 //自己加判断队列是否会溢出!????? 
	}
	if(count < T.vexnum) {//若输出的顶点数小于总共顶点数 
		printf("There is a cycle");//则图中存在有向回路 
		return 0;
	}
	else return 1;//拓扑排序正常结束 
}

1714662798810.png 利用栈做DFS,退出深度优先搜索(即出栈的元素)时记一次

(二)AOE网

1.定义:表示工程计划的有向图,其中,顶点表示事件,弧表示活动,弧上的权值表示完成一项活动需要的时间
2.特点:无环,存在唯一开始顶点(源点,入度为0)和唯一完成顶点(汇点,出度为0)
只有在某顶点代表的事件发生后,从该顶点发出去的弧所代表的各项活动才能开始;只有进入某顶点的各条弧所代表的活动都已经结束,该顶点所代表的事件才能发生。
3.路径长度:这条路径上完成各个活动所需的时间之和
4.AOE网中的某些活动可以并行进行,完成工程的最短时间是从开始顶点到完成顶点的最长路径长度路径长度最长的路径为关键路径。关键路径上所有活动都叫做关键活动
5.事件的最早发生时间 1714663555261.png 6.事件允许的最晚发生时间 1714663754244.png 7.活动最早发生时间 1714663837271(1).png 8.活动允许的最晚开始时间 1714663892486(1).png 9.l[i]-e[i]就是在不增加完成工作所需的总时间的情况下,活动可以延迟的时间。若l[i]=e[i],则活动ai为关键活动,l[i]-e[i]>0的活动不为关键活动 1714664570455.png 说明
(1)关键路径上所有的活动都是关键活动。因此提前完成非关键活动并不能加快工程的速度。
(2)网络中的关键路径并不唯一,对于有几条关键路径的网来说,仅仅提高某一条关键路径上关键活动的速度,是不能缩短整个工程工期的,而必须同时提高几条关键路径上关键活动的速度。所以,并不是网中任何一个关键活动的提前完成,整个工程都能提前完成。

(三)图存放表达式

1714754252998.png

1714754319441.png 1.算法 nibolan(G,v)步骤//从顶点v出发构造逆波兰式
若v的出度为0,则输出v的内容到逆波兰式,否则: 1 找到其发出去的第一条弧流向的顶点w,nibolan(G,w)2.找到其发出去的第二条弧流向的顶点w,nibolan(G,w) 3.输出v的内容到逆波兰式
1714754456995.png 1714754445447.png

六、图的应用——最短路径

(一)迪杰斯特拉算法

1714752785827.png 1714753058858.png 1714753088489.png 1.图的存储:邻接矩阵/邻接表
2.S中的点表示
方法一: 设一个一维数组int final[max];final[i]=1表示从源点到顶点i的最短路径已经求出,i在S中;final[i]=0表示从源点到顶点i的最短路径尚未求出,i在V-S中
方法二: 利用邻接矩阵主对角线的位置G.arcs[i][i]表示i是否在S中;G.arcs[i][i]=1表示从源点到顶点i的最短路径已经求出,i在S中;G.arcs[i][i]=0表示从源点到顶点i的最短路径尚未求出,i在V-S中
3.最短路径的表示 : 一维数组int D[max]表示最短路径的长度D[i] :从源点到点vi的最短路径的长度初态为:若从源点到vi有弧,则D[i]为弧上的权值;否则置D[i]为∞ ,即:D[i]=G.arcs[k][i];
//说明:k为源点二维数组int P[max][max]表示最短路径包含的顶点P[i][ ] :从源点到点vi的最短路径
P[i][j]=0: vj不在从源点到点vi的最短路径上
P[i][j]=1 : vj位于从源点 到点vi的最短路径上
说明:1.书上的这种表示最短路径的方式只是给出了最短路径经过的顶点有哪些,没有给出这些顶点在这条路径上的顺序。2.也可以采用其它的方式存储表示最短路径----例如利用最短路径定理,存放终点前一步的位置
1714753581807.png 1714753637010.png

void ShortestPath(AGraphs G, int k, int P[][], int D[]) {
	int j, w, j, min;
	for(i = 0; i < G.vexnum; i++) {
		final[i] = 0;
		D[i] = G.arcs[k][i];
		for(w = 0; w < G.vexnum; w++) {
			p[i][w] = 0;
			if(D[i] < INFINITY) {
				P[i][k] = 1;
				P[i][i] = 1;
			}
			D[k] = 0;
			final[k] = 1;
			for(i = 1; i <G.vexnum; i++) {
				min = INFINITY;
				for(w = 0; w < G.vexnum; i++) {
					if(!final[w] && D[w] < min) {
						j = w;
						min = D[w];
					}
				if(min == INFINITY) return;
				final[j] = 1;
				for(w = 0; w <G.vexnum; w++){
					if(!final[w] && (min + G.arcs[j][w] < D[w])) {
						D[w] = min + G.arcs[j][w];
						P[w] = P[j];
						P[w][w] = 1;
					}
				}
				}
			}
	}
}
}

(二)弗洛伊德算法

1714753887782.png 1.图的存储方式:邻接矩阵

#define max 100
typedef struct {
	int arcs[max][max];
	int vexnum,arcnum;
}AGraphs;
Agraphs G;

1714754031049.png 1714754045828.png

#define INFINITY  32767

void s1(intD[][], intP[][][], Agraphs G) {
	int i, j, k;
	for(i = 0; i < G.vexnum; i++)
		for(j =0; j < G.vexnum; j++) {
			D[i][j] = G.arcs[i][j];
			for(k=0;k<G.vexnum;k++) p[i][j][k]=0;
			if(D[i][j]<INFINITY) {
				p[i][j][i] = 1;
				p[i][j][j] = 1;
				}
			}
			for(k=0;k<G.vexnum;k++)
				for (i=0;i<G.vexnum;i++)
					for(j=0;j<G.vexnum;j++)
						if(D[i][k] + D[k][j] < D[i][j]) {
							D[i][j] = D[i][k] + D[k][j];
								for(int w=0; w<G.vexnum; w++)
									p[i][j][w] = p[i][k][w] || p[k][j][w];
									}
		}
}