数据结构笔记4
5 图
5.1 有关图
现实导航
公路规划及造价预算
网络路由表同步
教学计划安排
……
5.1.1 图的定义
-
图的特点
非线性结构
结点之间的邻接关系可以是任意的 -
图的定义
由两个集合构成,一个非空但有限的顶点集合V(不可以空),另一个是描述顶点关系的边集合E(可以空) -
图的数学描述方式
G=(V, E),每条边是一顶点对(v, w),且v, w 属于V
通常用|V|表示顶点的数量,|E|表示边的数量
5.1.2 图定义解释
图G含有n (n > 0)个结(顶)点,它们组成顶点的有限集合V(G),其中n个顶点之间的关系组成集合E(G),E(G):
-
可以是空集;若E(G)为空,则图G只有顶点,没有边
-
若图G中的每条边都有方向,则称G为有向图
-
若图G中的每条边都没有方向,则称G为无向图
-
有向边表示为 <vi, vj> (注意 <vi, vj>和 <vj, vi> 为两条不同的边);无向边表示为 (vi, vj)



5.1.3 图的特点
-
顶点可以有多个前驱,或者后继
-
顶点间是多对多(N:M)的关系
-
无向图顶点关系是对称的,有向图顶点关系不对称
5.1.4 图的术语
| 术语 | 英文 | 定义 |
|---|---|---|
| 顶点 | Vertex | 即图上的数据元素 |
| 无向图 | Undirected Graph | 顶点间关系是无方向,即图中任意两顶点间关系是对称的。无向边表示为 (v, w),说明顶点间对称的关系 |
| 边 | Edge | 顶点间对称关系的描述 |
| 有向图 | Directed Graph | 顶点间关系是有序的,即图中顶点间存在不对称关系。顶点间有序关系表示为 <v, w>,即有向边 |
| 弧 | Arc | 顶点间不对称关系的描述 |
| 弧尾 | Tail | 有向边的初始点(出端) |
| 弧头 | Head | 有向边的终端点(入端) |
| 术语 | 定义 | 示例 / 补充说明 |
|---|---|---|
| 邻接点 | 存在关系的两个顶点互为相邻,即邻接 | - |
| 路径 | 一个顶点序列,包含从一个顶点到另一个顶点所经过的全部顶点序列 | 如:v1v3v4v1v2、v1v4v3v5v4v3v2 |
| 简单路径 | 序列中顶点不重复出现的路径 | 如:v1v3v4v2、v1v4v5v3v2 |
| 回路(环) | 第一个顶点和最后一个顶点相同的简单路径 | 如无向图:v1v4v5v3v2v1 |
| 完全图 | 顶点间可以存在的关系全存在,拥有该类型图中最多的边数 | 无向完全图边数:n(n−1)/2;有向完全图边数:n(n−1)(为无向的 2 倍) |


| 术语 | 定义 | 补充说明 |
|---|---|---|
| 顶点的度 | 与顶点相关联的边数 | - |
| 入度 | 以顶点为弧头的弧的数目 | 仅适用于有向图 |
| 出度 | 以顶点为弧尾的弧的数目 | 仅适用于有向图 |
| 稀疏图 | 边或弧数量很少的图 | - |
| 稠密图 | 非稀疏图 | - |
| 加权图 | 边或弧上带有相关数据(权值)的图 | - |
| 网络 (Network) | 带权的连通图 | - |
| 子图 | 顶点和边都属于另一幅图的图 | 数学定义:设 G=(V,E),G1=(V1,E1),且 V1⊆V,E1⊆E,E1 关联的顶点都在 V1 中,则称 G1 是 G 的子图 |
| 连通 | 两顶点间存在路径,则称两顶点连通 | - |
| 连通图 | 无向图中任意两顶点都连通 | - |
| 连通分量 | 无向图的极大连通子图 | - |
| 强连通图 | 有向图中任意两顶点间都存在往返路径 | - |
| 强连通分量 | 有向图的极大强连通子图 | - |
| 生成树 | 连通图的生成树是包含全部顶点的极小连通子图 | 包含且仅包含 n-1 条边;对有向图,生成树仅有一个顶点入度为 0,其余顶点入度均为 1 |
5.2 图的存储结构及实现
问题抽象—>建模
5.2.1 邻接矩阵
静态结构 用数组存储
邻接矩阵——表示顶点之间相邻关系的矩阵,用数组表示。设G=(V, E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:

若G是网络,则邻接矩阵定义为

其中 Wij表示边上的权值,表示一个计算机允许的、大于所有边上权值的数,如

5.2.2 算法实现
- 邻接矩阵定义(静态结构)
#define MaxVertexNum 100
#define INFINITY 65535
typedef char VexType;
typedef float AdjType;
typedef struct {
VexType vex[MaxVertexNum]; //一维数组存储图的顶点向量
AdjType arcs[MaxVertexNum][MaxVertexNum]; //邻接矩阵存储图的边
int n, e; //图的当前顶点数和弧数
}MGraph;
- 创建无向图邻接矩阵算法(顺序表)
void createMGraph(Mgraph *G) {
int i, j, k, w;
scanf(&G->n, &G->e); //顶点数,边数
for (i=0; i<G->n; i++) scanf(&G->vex[i]); //构造顶点向量
for (i=0; i<G->n; i++)
for (j=0; j<G->n; j++)
G->arcs[i][j]=INFINITY; //初始化邻接矩阵
for (k=0; k<G->e; k++) { //构造邻接矩阵
scanf(&i, &j, &w); //输入一条边依附的顶点和权值
G->arcs[i][j]=w; //边(v1, v2)的权值
G->arcs[j][i]=G->arcs[i][j]; //置(v1, v2)的对称边(v2, v1)
}
return ;
}//Createl
-
邻接链表
(动态结构)
简称邻接表,用链表存储顶点之间的关系,类似于树的孩子链表表示法

-
邻接表数据结构定义
#define MaxVertexNum 10 typedef float AdjType //权值的类型 typedef struct ArcNode { //边表结点 int endvex; //邻接点域 struct ArcNode *Next; //链域 AdjType weight; //权域 }EdgeList; typedef char VexType //顶点数据类型 typedef struct Vnode { //顶点表结点 VexType vertex; //顶点域 EdgeList *firstEdge; //边表头指针 }VexNode; typedef struct { VexNode Adjlist[MaxVertexNum]; //邻接表 int n, e; //图中当前顶点数和边数 }AdjListGraph; -
创建有向图的邻接表算法
void CreateALGraph(AdjListGraph *G) { int i, j, k; EdgeList *edge; AdjType w; // 权值(对应你结构体的float) // 1. 读顶点数、边数 ✅ scanf("%d %d", &G->n, &G->e); // 2. 建立顶点表 ✅ for (i = 0; i < G->n; i++) { scanf(" %c", &G->Adjlist[i].vertex); // 正确读字符 G->Adjlist[i].firstEdge = NULL; } // 3. 建立有向边(只建 i→j,不建反向,正确)✅ for (k = 0; k < G->e; k++) { // 读入边的起点、终点、权值 scanf("%d %d %f", &i, &j, &w); edge = (EdgeList *)malloc(sizeof(EdgeList)); edge->endvex = j; edge->weight = w; // 赋值权值(匹配结构体) edge->Next = G->Adjlist[i].firstEdge; G->Adjlist[i].firstEdge = edge; } }
5.3 图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次
(1)深度优先遍历
(2)广度优先遍历
5.3.1 深度优先(栈)
-
定义 对于图G
-
首先访问出发点,并将其标记为已访问过;
-
然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问为止;
-
然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问为止;
------v1->v2->v4->v8->v5->v3->v6->v7-----
-
-
特点
-
定义是递归的
-
尽可能先对纵深方向进行搜索
-
只有当图的存储结构确定,访问结果才唯一
-
-
图的深度优先遍历算法
void DFS(Graph G, int v) // 图G,从编号v的顶点开始遍历 { int v1; // 临时存:邻居顶点编号 Visited[v] = TRUE; // 标记当前顶点v【已经访问过】 VisitFunc(v); // 访问当前顶点v(比如打印、输出) // 遍历 v 的所有邻接点 // 第一步:取v的第一个邻居 v1 // 后续:取下一个邻居 v1,直到没有(v1==NULL/-1) for (v1 = FirstAdjV(G, v); v1 != NULL; v1 = NextAdjV(G, v, v1)) { if (!Visited[v1]) // 如果这个邻居没被访问过 DFS(G, v1); // 递归:从这个邻居继续深度优先搜 } } -
函数FirstAdjV(G, v) 的实现(邻接矩阵)
int FirstAdjV(MGraph *G, int v) { //for (i=0; i<G->n; i++) //查找顶点V //if (G->vex[i]==V) break; for (j=0; j<G->n; j++) //查找矩阵中第一个非0值 if (G->arcs[v][j] !=0 ) break; return j; //返回第一邻接点 } -
函数NextAdjV(G, v, v1)的实现(邻接矩阵)
int NextAdjV(MGraph *G, int v,int v1) { //for (i=0; i<G->n; i++) //查找顶点V所在行 //if (G->vex[i]==V) break; //for (j=0; j<G->n; j++) //查找顶点V1所在列 //if (G->vex[j] ==V1 ) break; for (k=v1+1; k<G->n; k++) //查找V1下一个非0值 if (G->arcs[v][k] !=0 ) break; return k; //返回下一顶点 }DFS(G, i)深度优先遍历函数
void DFS (AdjListGraph *G, int i) { //以Vi为出发点对邻接表存储的图G进行DFS搜索 EdgeList *W; printf(“visit vertex:V%c\n”, G->Adjlist[i].vertex); //相当于VisitFunc(i), 即访问顶点Vi Visited[i]=TRUE; //标记Vi已访问 for (W=G->Adjlist[i].firstEdge; W; W=W->Next) if(!Visited[W->endvex]) DFS(G, W->endvex); } -
函数FirstAdjV(G, v) 的实现(邻接链表)
EdgeList *FirstAdjV(AdjListGraph *G, VexType V) { for (i=0; i<G->n; i++) //查找顶点V if (G->Adjlist[i].vertex==V) break; p=G->Adjlist[i].firstEdge; //指向第一邻接点 return p; //返回第一邻接点 } -
函数NextAdjV(G, v, v1)的实现(邻接链表)
EdgeList *NextAdjV(AdjListGraph *G, VexType V,VexType V1) { for (i=0; i<G->n; i++) //查找顶点V if (G->Adjlist[i].vertex==V) break; p=G->Adjlist[i].firstEdge; while (p) //查找顶点V1 if (Adjlist[p->endvex].vertex != V1) p=p->next; else { p=p->next; break; } return p; //返回V1的下一顶点 } -
深度优先遍历算法(邻接矩阵/邻接链表)
Boolean Visited[MAX]; Status (* VisitFunc)(int v); void DFS_ALG(AdjListGraph *G) { //深度优先遍历图G, 邻接表或邻接矩阵表示G时算法完全相同 int i; for (i=0; i<G->n; i++) Visited[i]=FALSE; //标志向量初始化,标记全尚未访问 for (i=0; i<G->n; i++) if (!Visited[i]) //Vi未访问过 DFS(G, i); //以Vi为源点开始DFS搜索 }//DFSTraverse
5.3.2 广度优先(队列)
-
定义
对于图G
-
从某个顶点v出发,在访问了v之后,依次访问v的邻接点;
-
然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点出发依次访问它们的邻接点”,直至图中所有已被访问的顶点的邻接点都被访问到;
-
若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的起始点,重复上述过程,直至图中所有顶点均已被访问为止。
-
-
特点
-
定义是递归的
-
尽可能先对邻接结点进行搜索
-
只有当图的存储结构确定,访问结果才唯一
-
-
广度优先遍历算法的实现
Void BFS(Graph G){ Queue=*Q; VexType U, V, W; for (U=0; U<G.n; ++U) Visited[U]=FLASE; Q=CreateQueue(MaxSize); //创建空队列 for (U=0; U<G.n; ++U) //开始遍历 if (!Visited[U]=TRUE) { //若U尚未访问 Visited[U]=TRUE; //标记 VisitFunc(U); //访问 AddQ(Q,U); //入队 while (!IsEmpty(Q)){ V=DeleteQ(Q); //队头元素出队,并置为V for (W=FirstAdjV(G, V); W; W=NextAdjV(G, V, W)) if (!Visited[W]) { Visited[W]=TRUE; VisitFunc(W); AddQ(Q, W); } } //while } //if,结束从U开始的BFS } //BFS
5.4 图的应用
5.4.1 最小生成树
-
定义
连通图G的一个子图如果是一棵包含G的所有顶点的树,则该子图称为G的生成树(Spanning Tree)
-
特点
n个顶点的图的生成树仅有n-1条边。即生成树是连通图的包含图中的所有顶点的极小连通子图,生成树中没有回路
-
构造生成树的方法
-
深度优先生成树
通过对图进行深度优先遍历,可以遍访图中顶点。若此时在图中只保留访问路径,其它边删除,则可以得到一棵树,该树即为图的深度优先生成树。
-
广度优先生成树
通过对图进行广度优先遍历,可以遍访图中顶点。若此时在图中只保留访问路径,其它边删除,则可以得到一棵树,该树即为图的广度优先生成树。

-
5.4.2 最小生成树MST
- 定义:对于一个含有n个顶点的连通网G,如果存在一棵包含G的所有顶点的生成树,且生成树中的n-1条边的权值之和为该连通网G所有可能的生成树的权值之和的最小值,则这棵生成树称为该连通网的最小生成树。
-
求MST的一般算法思想
GenericMST(G) {
//求G的某棵MST
T; //T初始为空,是指顶点集和边集均空
while (T未形成G的生成树) {找出T的一条安全边(u,v); //即T{(u,v)}仍为MST的子集
T= T{(u,v); //加入安全边,扩充T}
return T; //T为生成树,且是G的一棵MST
}安全边:一条加入到T的边(u,v),它能保证T{(u,v)}仍能是MST
Prim算法思想

- 算法思想

-
实现
void prim(GraphMatrix *pgraph, Edge mst[ ]) { //mst[ ]存放生成树上n-1条边 int i,j,min, vx,vy; double weight; Edge edge; for (i=0; i<VN-1; ++i) { //读取v0出发的全部n-1条边信息到mst数组 mst[i].start_vex=0; mst[i].stop_vex=i+1; mst[i].weight=pgraph->arcs[0][i+1]; } for (i=0; i<VN-1; ++i) { weight=MAXINT; min=-1; for (j=i; j<VN-1; ++j) //在mst中找出(vx,vy)最小边 if (mst[j].weight<weight) { weight=mst[j].weight; min=j; } edge=mst[min]; mst[min]=mst[i]; mst[i]=edge; //最短边前移到mst[i]位置,选择排序 vx=mst[i].stop_vex; for (j=i+1; j<VN-1; ++j) { //确认找到的是最小边后完成替换 vy=mst[j].stop_vex; weight=pgraph->arcs[vx][vy]; if (weight<mst[j].weight) { mst[j].weight=weight; mst[j].start_vex=vx; } //修改其它点到达边的信息,即动态调整 } }}Kruskal算法思想


-
算法思想

-
实现
int Kruskal(GraphMatrix *pgraph, Edge mst[]) { int i,j,num=0,start,stop; double minweight; int status[VN]; for (i=0; i<VN; ++i) status[i]=i; //每个顶点属于自己代表的连通子图 while (num<VN-1) { minweight=MAXINT; //设最小权值初始值 for (i=0; i<VN; ++i) //挑选权值最小的边 for (j=i+1; j<VN; ++j) if (pgraph->arcs[i][j]<minweight) { start=i; stop=j; minweight=pgraph->arcs[i][j]; } if (minweight==MAXINT) return 0; //无可选的边,则退出 if (status[start]!=status[stop]) { //判断挑选的两点不在同一个连通子图 mst[num].start_vex=start; mst[num].stop_vex=stop; mst[num].weight=pgraph->arcs[start][stop]; ++num; for (j=status[stop], i=0; i<VN; ++i) if (status[i]==j) status[i]=status[start]; } //if pgraph->arcs[start][stop]=MAXINT; //标记选过得边 } //while return 1; } //Kruskal
5.4.3 拓扑排序
-
拓扑排序:对于一个有向无环图(Directed A Cyclic Graph, 简称DAG)G进行拓扑排序是将G中所有顶点排成一个线性序列,使得对图中任意一对顶点u和v,若<u,v>E(G),则u在线性序列中出现在v之前。
-
拓扑序列:按拓扑排序获得的线性序列称为满足拓扑次序的序列,简称拓扑序列。
上图的拓扑序列是:(C1, C2, C3, C4, C5, C7, C9, C10, C11, C6, C12, C8)
什么是拓扑排序?就是由某个集合上的一个偏序得到该集合上的一个全序,这一操作称为拓扑排序
偏序(Partial Order):若集合X上的关系R是自反的、自对称的和传递的,则称关系R是集合X上的偏序关系
全序:设R是集合X上的偏序,如果对每个x,yX必有xRy或yRx,则称R是集合X上的全序关系
在拓扑排序中,用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的图(Activity On Vertex),简称AOV图。
-
有向图常用于说明事件发生的先后关系,如:
-
拓扑序列的特点:结果不唯一
-
可以获得拓扑序列的有向图中一定不存在环
上图的拓扑序列是
(C1, C2, C3, C4, C5, C7, C9, C10, C11, C6, C12, C8)或
(C9, C10, C11, C6, C1, C12, C4, C2, C3, C5, C7, C8)
-
拓扑排序算法思想
对有向图的拓扑序列进行分析可以发现,在一个拓扑序列里,每个顶点必定出现在它的所有后继顶点之前。可见拓扑排序方法(1)是
- 无前驱的顶点优先,即每次选择一个无前驱的结点,且输出之,之后去掉该顶点为出度的弧。抽象算法是
NonPreFirstTopSort(G) { //优先输出无前驱的顶点
while (G中有入度为0的顶点) do {
从G中选择一个入度为0的顶点v且输出之;
从G中删除v及其所有以它为出度的弧;
}
if (输出的顶点数目<|V(G)|)
//若此条件不成立,则表示所有顶点均已输出,排序成功
Error(“G中存在有向环,排序失败!”);
}
typedef char VexType; //顶点数据类型
#define MaxVertexNum 100 //最大顶点数为100
typedef struct Node { //边表结点
int endvex;
struct Node *Next;
}EdgeList;
typedef struct Vnode { //顶点表结点
VexType vertex;
EdgeList *firstEdge;
int indegree;
}VertexNode;
typedef struct {
VertexNode Adjlist[MaxVertexNum]; //邻接表
int n, e; //图中当前顶点数和边数
}ALGraph;


- 拓扑排序算法
void TopoSort(Graph G, int TopNum[]) {
//有向图G采用邻接表存储结构
//若G无回路,拓扑序列存入TopNum,并返回OK,否则ERROR
findInDegree(G, Indegree); //对各顶点求入度indegree[0..vernum-1]
Q=CreateQueue(G.n);
for (i=0; i<G.n; i++) //建0入度顶点队列q
if (! Indegree[i]) AddQ(Q, i); //入度为0者进队列
counter=0; //对输出的顶点计数
while (!IsEmpty(Q)) {
i=DeleteQ(Q); TopNum[i]= ++counter; //记录第i个顶点,并计数
for (p=G. Adjlist[i].firstEdge; p; p=p->Next) {
k=p->endvex; //对第i个顶点的每个邻接点的入度减1
if (!(- -Indegree[k])) AddQ(Q, k); //若入度减为0,则入队列
} //for
} //while
if (counter< G.n) return ERROR; //该有向图有回路
else return OK;
} //TopologicalSort
findInDegree(G, indegree); //对各顶点求入度indegree[0..vernum-1]
for (i=0; i<G.N; i++)
G.Adjlist[i].indegree=0; //indegree数组已经初始化为0
for (i=0; i<G.n; i++) //对每个顶点扫描i的出边表
for (p=G.Adjlist[i].firstEdge; p; p=p->Next)
{ //设p->endvex=j, 则将<i, j>的终点j入度+1
G.Adjlist [p->endvex].indegree++;
}
5.4.4 关键路径
- 有一种有向图如下图所示,它的顶点代表若干事件(Event),弧代表一些活动,弧的权值表示活动持续的时间。因为这种有向图是边(弧)表示活动的网络(Activity On Edge),故简称为AOE网
由于工程只有一个开始点和一个完成点,所以在正常的情况下(无环),网中只有一个入度为零的点(称做源点)和一个出度为零的点(叫做汇点)
弧ai,代表开展的活动,弧上的权值x为活动持续的时间
结点Vi是事件点,表示入度边的活动均已结束,出度边的活动可以开始
假设图表示工程活动,最少需要多少时间来完成?
-
关于AOE网
(1) 完成整项工程至少需要多少时间?
完成工程需要的时间是从开始点到完成点的所经路径上的持续时间之和。其中最长的路经是工程需要的最短时间,该最长路经长度的路径叫做关键路径(Critical Path)
(2) 哪些活动是影响工程进度的关键?
在关键路径上的活动是影响工程进度的关键活动 -
关键路径的查找方法
正向拓扑算法,求出每个事件的最早发生时间ve[i]
逆向拓扑排序,求出每个事件的最晚发生时间vl[i]

浙公网安备 33010602011771号