1. 图的定义和基本术语
1.1 图的定义
-
**图(Graph)**G 由两个集合 V 和 E 组成,记为 G = ( V ,E )
- V :顶点的有穷非空集合
- V(G): 图 G 的顶点集合
- E :V 中顶点偶对的有穷集合
- E(G): 图 G 的边集合
- E(G)可以为空集:图 G 只有顶点而没有边
- E(G)为有向边的集合:有向图
- E(G)为无向边的集合:无向图
- E(G): 图 G 的边集合
- V :顶点的有穷非空集合
-
在有向图中,顶点对 <x, y> 是有序的
-
<x, y> :从顶点 x 到顶点 y 的一条有向边
- x 是有向边的始点,y 是有向边的终点
- <x, y> 也称为一条弧,x 为弧尾,y 为弧头
-
-
在无向图中,顶点对 (x, y) 是无序的
- (x, y) :顶点 x 和顶点 y 相关联的一条边
1.2 图的基本术语
-
用 n 表示图中顶点的数目,用 e 表示边的数目
-
子图:假设有 2 个图 G = ( V ,E )和 G’ = ( V’ ,E’ ),V’ ⊆ V 且 E’ ⊆ E ,G‘ 为 G 的子图
-
无向完全图:对于无向图,具有 n(n-1)/2 条边
-
有向完全图:对于有向图,具有 n(n-1) 条弧
-
稀疏图:有很少边或弧(如 e < nlog_2(n))的图
-
稠密图:有较多边或弧的图
-
权:每条边上具有某种含义的数值
-
网:带权的图
-
邻接点:对于无向图 G ,边(v, v’)∈ E,v 和 v’ 互为邻接点,v 和 v‘ 相邻接
- 边(v, v’)依附于顶点 v 和 v’
- 边(v, v’)与顶点 v 和 v’ 相关联
-
度:和顶点 v 相关联的边的数目,记为 TD(v)
-
入度:以顶点 v 为头的弧的数目,记为 ID(v)
-
出度:以顶点 v 为尾的弧的数目,记为 OD(v)
-
路径:从顶点 v 到顶点 v’
- 无向图:顶点序列(v = v_ { i , 0 }, v_ { i , 1 }, ··· , v_ { i , 0 } = v’),(v_ { i , j-1 } , v_ { i , j })∈ E ,1 ≤ j ≤ m
- 有向图:路径也是有向的,顶点序列应满足 <v_ { i , j-1 } , v_ { i , j }> ∈ E ,1 ≤ j ≤ m
-
路径长度:一条路径上经过的边或弧的数目
-
回路(环):第一个二顶点和最后一个顶点相同的路径
-
简单路径:序列中顶点不重复出现的路径
-
简单回路(简单环):除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
-
连通:在无向图 G 中,从顶点 v 到顶点 v’ 有路径,v 和 v’ 是连通的
-
连通图:对于图中任意两个顶点 v_i ,v_j ∈ V ,v_i 和 v_j 都是连通的
-
连通分量:无向图中的极大连通子图
-
强连通图:在有向图 G 中,对于每一对 v_i ,v_j ∈ V,v_i ≠ v_j ,从 v_i 到 v_j 和 v_j 到 v_i 都存在路径
-
强连通分量:有向图中的极大强连通子图
-
连通图的生成树:一个极小连通子图,其含有图中全部顶点,但只有足以构成一棵树的 n-1 条边
-
有向树:有一个顶点的入度为 0 ,其余顶点的入度均为 1 的有向图
-
生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧
2. 图的存储结构
2.1 邻接矩阵
2.1.1 邻接矩阵表示法
-
邻接矩阵(Adjacency Matrix):表示顶点之间相邻关系的矩阵
-
G( V ,E )是具有 n 个顶点的图,G 的邻接矩阵是具有如下性质的 n 阶方阵
A [ i ] [ j ] = { 1 若 < v i , v j > 或 ( v i , v j ) ∈ E 0 反 之 A[i][j]=\begin{cases} 1 \quad 若 <v_i, v_j> 或 (v_i, v_j) ∈ E\\ 0 \quad 反之\end{cases} A[i][j]={1若<vi,vj>或(vi,vj)∈E0反之 -
G 是网,则其邻接矩阵为
A [ i ] [ j ] = { w i , j 若 < v i , v j > 或 ( v i , v j ) ∈ E ∞ 反 之 A[i][j]=\begin{cases} w_{i,j} \quad 若 <v_i, v_j> 或 (v_i, v_j) ∈ E\\ ∞ \quad 反之\end{cases} A[i][j]={wi,j若<vi,vj>或(vi,vj)∈E∞反之 -
图的邻接矩阵存储表示
#define MAXSIZE 10
typedef struct {
int vexs[MAXSIZE]; // 顶点表
int arcs[MAXSIZE][MAXSIZE]; // 邻接矩阵
int vexnum; // 图的当前点数
int arcnum; // 图的当前边数
} AMGraph;
2.1.2 采用邻接矩阵表示法创建无向网
void CreateUDN(AMGraph* G) {
printf("Enter vexnum , arcnum: ");
scanf("%d,%d", &(G->vexnum), &(G->arcnum)); // 输入总定点数,总边数
for (int i = 0; i < G->vexnum; i++) // 输入点的信息
{
printf("Enter vex: ");
scanf("%d", &(G->vexs[i]));
}
for (int i = 0; i < G->vexnum; i++) // 初始化邻接矩阵,边的权值置为最大值
{
for (int j = 0; j < G->vexnum; j++)
{
G->arcs[i][j] = INT_MAX;
}
}
for (int i = 0; i < G->arcnum; i++) // 构造邻接矩阵
{
int vex1, vex2, value;
printf("Enter vex1, vex2, value: ");
scanf("%d,%d,%d", &vex1, &vex2, &value); // 输入一条边依附的顶点和权值
int i = LocateVex(G, vex1); // 获取 vex1 在 G 中的位置(下标)
int j = LocateVex(G, vex2); // 获取 vex2 在 G 中的位置(下标)
G->arcs[i][j] = G->arcs[j][i] = value; // 边 <v_1, v_2> 和对称边 <v_2, v_1> 的权值置为 value
}
printf("Create Success\n");
}
int LocateVex(AMGraph* G, char vex) {
int i = 0;
for (; i < G->vexnum; i++) {
if (G->vexs[i] == vex) {
break;
}
}
return i;
}
2.1.3 邻接矩阵表示法的优缺点
-
优点
- 便于判断两个顶点之间是否有边,根据 A[ i ] [ j ] = 0 或 1 来判断
- 便于计算各个顶点的度
- 无向图:邻接矩阵第 i 行元素之和就是顶点 i 的度
- 有向图:第 i 行元素之和就是顶点 i 的出度, 第 i 列元素之和就是顶点 i 的入度
-
缺点
- 不便于增加和删除顶点
- 不便于统计边的数目,需要扫描临界矩阵所有元素才能统计完毕,时间复杂度为 O(n^2)
- 空间复杂度高
- 有向图:n 个顶点需要 n^2 个单元存储边
- 无向图:n 个顶点采用压缩存储方法需要 n(n-1)/2 个单元
- 空间复杂度均为 O(n^2)
测试代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
void CreateUDN(AMGraph);
int LocateVex(AMGraph);
void PrintGraph(AMGraph);
typedef struct {
int vexs[MAXSIZE];
int arcs[MAXSIZE][MAXSIZE];
int vexnum;
int arcnum;
} AMGraph;
int main() {
AMGraph G;
CreateUDN(&G);
printf("****************\n");
PrintGraph(G);
printf("****************\n");
}
void CreateUDN(AMGraph* G) {
printf("Enter vexnum , arcnum: ");
scanf("%d,%d", &(G->vexnum), &(G->arcnum));
for (int i = 0; i < G->vexnum; i++)
{
printf("Enter vex: ");
scanf("%d", &(G->vexs[i]));
}
for (int i = 0; i < G->vexnum; i++)
{
for (int j = 0; j < G->vexnum; j++)
{
G->arcs[i][j] = INT_MAX;
}
}
for (int i = 0; i < G->arcnum; i++)
{
int vex1, vex2, value;
printf("Enter vex1, vex2, value: ");
scanf("%d,%d,%d", &vex1, &vex2, &value);
int i = LocateVex(G, vex1);
int j = LocateVex(G, vex2);
G->arcs[i][j] = G->arcs[j][i] = value;
}
printf("Create Success\n");
}
int LocateVex(AMGraph* G, char vex) {
int i = 0;
for (; i < G->vexnum; i++) {
if (G->vexs[i] == vex) {
break;
}
}
return i;
}
void PrintGraph(AMGraph G) {
for (int i = 0; i < G.vexnum; i++)
{
for (int j = 0; j < G.arcnum; j++)
{
printf("%10d ", G.arcs[i][j]);
}
printf("\n");
}
printf("Print Success\n");
}
2.2 邻接表
2.2.1 邻接表表示法
- 邻接表(Adjacency List):图的一种链式存储结构
- 对图中每个顶点 v_i 建立一个单链表,把与 v_i 相邻接的顶点放在这个链表中
- 每个链表的第一个结点存放有关顶点的信息,把这一结点看成链表的表头,其余结点存放有关边的信息
- 构成
- 表头结点表:由表头结点以顺序结构的形式存储
- 数据域(data):存储顶点 v_i 的名称或其他有关信息
- 链域(firstarc):指向链表中第一个结点(与顶点 v_i 邻接的第一个邻接点)
- 边表:由表示图中顶点间关系的 2n 个边链表组成
- 邻接点域(adjvex):与顶点 v_i 邻接的点在图中的位置
- 数据域(info):存储和边相关的信息,如权值等
- 链域(nextarc):与顶点 v_i 相接的下一条边的结点
- 表头结点表:由表头结点以顺序结构的形式存储
#define MAXSIZE 10 // 最大顶点数
typedef struct ArcNode { // 边结点
int adjvex; // 该边所指向的顶点的位置
struct ArcNode* nextarc; // 指向下一条边的指针
char info; // 和边相关的信息
} ArcNode;
typedef struct VNode { // 顶点信息
int data;
ArcNode* firstarc; // 指向第一条依附该顶点的边的指针
} VNode, AdjList[MAXSIZE]; // AdjList 表示邻接表类型
typedef struct { // 邻接表
AdjList vertices;
int vexnum, arcnum; // 图的当前顶点数和边数
} ALGraph;
2.2.2 采用邻接表表示法创建无向图
void CreateUDG(ALGraph* G) {
printf("Enter vexnum , arcnum: ");
scanf("%d,%d", &(G->vexnum), &(G->arcnum)); // 输入总定点数,总边数
for (int i = 0; i < G->vexnum; i++) // 输入各点,构造表头结点表
{
printf("Enter vex: ");
scanf("%d", &(G->vertices[i].data)); // 输入顶点值
G->vertices[i].firstarc = NULL; // 初始化表头结点的指针域为 NULL
}
for (int i = 0; i < G->arcnum; i++) // 输入各边,构造邻接表
{
int vex1, vex2;
printf("Enter vex1, vex2: ");
scanf("%d,%d", &vex1, &vex2); // 输入一条边依附的两个顶点
int i = LocateVex(G, vex1); // 获取 vex1 在 G 中的序号(从 0 开始)
int j = LocateVex(G, vex2); // 获取 vex2 在 G 中的序号(从 0 开始)
ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode)); // 生成新的边结点 p1
p1->adjvex = j; // 邻接点序号为 j
p1->nextarc = G->vertices[i].firstarc;
G->vertices[i].firstarc = p1; // 将新结点 p1 插入顶点 v_i 的边表头部
ArcNode* p2 = (ArcNode*)malloc(sizeof(ArcNode)); // 生成新的边结点 p2
p2->adjvex = i; // 邻接点序号为 i
p2->nextarc = G->vertices[j].firstarc;
G->vertices[j].firstarc = p2; // 将新结点 p2 插入顶点 v_j 的边表头部
}
printf("Create Success\n");
}
int LocateVex(ALGraph* G, int vex) {
for (int i = 0; i < G->vexnum; i++) {
if (G->vertices[i].data == vex)
return i;
}
}
2.2.3 邻接表表示法的优缺点
-
优点
- 便于增加和删除顶点
- 便于统计边的数目,按顶点表顺序扫描所有边表可以得到边的数目,时间复杂度为 O(n+e)
- 空间效率高
- 无向图:n 个顶点 e 条边的图,邻接表中有 n 个顶点表结点和 2e 个边表结点
- 有向图:n 个顶点 e 条边的图,邻接表中有 n 个顶点表结点和 e 个边表结点
- 空间复杂度均为 O(n+e)
-
缺点
- 不便于判断顶点之间是否有边,要判定 v_i 和 v_j 之间是否有边,就需扫描第 i 个边表,最坏情况下要耗费 O(n)时间
- 不便于计算有向图各个顶点的度
- 无向图:顶点 v_i 的度是第 i 个边表中的结点个数
- 有向图:第 i 个边表上的结点个数是顶点 v_i 的出度,但是求 v_i 的入度较困难,需要遍历各顶点的额边表
测试代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
void CreateUDG(ALGraph);
int LocateVex(ALGraph);
void PrintGraph(ALGraph);
typedef struct ArcNode {
int adjvex;
struct ArcNode* nextarc;
char info;
} ArcNode;
typedef struct VNode {
int data;
ArcNode* firstarc;
} VNode, AdjList[MAXSIZE];
typedef struct {
AdjList vertices;
int vexnum, arcnum;
} ALGraph;
int main() {
ALGraph G;
CreateUDG(&G);
printf("****************\n");
PrintGraph(G);
printf("****************\n");
}
void CreateUDG(ALGraph* G) {
printf("Enter vexnum , arcnum: ");
scanf("%d,%d", &(G->vexnum), &(G->arcnum));
for (int i = 0; i < G->vexnum; i++)
{
printf("Enter vex: ");
scanf("%d", &(G->vertices[i].data));
G->vertices[i].firstarc = NULL;
}
for (int i = 0; i < G->arcnum; i++)
{
int vex1, vex2;
printf("Enter vex1, vex2: ");
scanf("%d,%d", &vex1, &vex2);
int i = LocateVex(G, vex1);
int j = LocateVex(G, vex2);
ArcNode* p1 = (ArcNode*)malloc(sizeof(ArcNode));
p1->adjvex = j;
p1->nextarc = G->vertices[i].firstarc;
G->vertices[i].firstarc = p1;
ArcNode* p2 = (ArcNode*)malloc(sizeof(ArcNode));
p2->adjvex = i;
p2->nextarc = G->vertices[j].firstarc;
G->vertices[j].firstarc = p2;
}
printf("Create Success\n");
}
int LocateVex(ALGraph* G, int vex) {
for (int i = 0; i < G->vexnum; i++) {
if (G->vertices[i].data == vex)
return i;
}
}
void PrintGraph(ALGraph G) {
for (int i = 0; i < G.vexnum; i++)
{
ArcNode* p = G.vertices[i].firstarc;
printf("%d", G.vertices[i].data);
while (p)
{
printf("->%d", p->adjvex);
p = p->nextarc;
}
printf("\n");
}
printf("Print Success\n");
}
2.3 十字链表
- 十字链表(Orthogonal List):有向图的另一种链式存储结构
- 将有向图的邻接表和逆邻接表结合起来得到的一种链表
- 在十字链表中,对应于有向图中每一条弧有一个结点,对应于每个顶点也有一个结点
- 构成
- 弧结点
- 尾域(tailvex):指示弧头的顶点在图中的位置
- 头域(headvex):指示弧头的顶点在图中的位置
- 链域(hlink):指向弧头相同的下一条弧
- 链域(tlink):指向弧尾相同的下一条弧
- 信息域(info):指向弧的相关信息
- 顶点结点
- 数据域(data):存储和顶点相关的信息
- 首入域(firstin):指向以该顶点为弧头的第一个弧结点
- 首出域(firstout):指向以该顶点为弧尾的第一个弧结点
- 弧结点
#define MAXSIZE 10
typedef struct ArcBox {
int tailvex, headvex; // 该弧的尾和头顶点的位置
struct ArcBox* hlik, * tlink; // 分别为弧头相同和弧尾相同的弧的链域
int info; // 存储弧相关的信息
} ArcBox;
typedef struct VexNode {
int data;
ArcBox* firstin, * firstout; // 分别指向该顶点第一条入弧和出弧
} VexNode;
typedef struct {
VexNode xlist[MAXSIZE]; // 表头向量
int vexnum, arcnum; // 有向图的当前顶点数和弧数
} OLGraph;
2.4 邻接多重表
- 邻接多重表(Adjacency Multilist):无向图的另一种链式存储结构
- 构成
- 边结点
- 标志域(mark):标记该条边是否被搜索过
- 顶点域(ivex):该边依附的顶点位置
- 顶点域(jvex):该边依附的顶点位置
- 边域(ilink):指向下一条依附于顶点 ivex 的边
- 边域(jlink):指向下一条依附于顶点 jvex 的边
- 信息域(info):指向和边相关的各种信息的指针域
- 顶点结点
- 数据域(data):存储和该顶点相关的信息
- 边域(firstedge):指示第一条依附于该顶点的边
- 边结点
#define MAXSIZE 10
typedef enum { unvisited, visited } VisitIf;
typedef struct EBox {
VisitIf mark; // 访问标记
int ivex, jvex; // 该边依附的两个顶点的位置
struct EBox* ilink, * jlink; // 分别指向依附这两个顶点的下一个边
int info; // 该边的信息
} EBox;
typedef struct VexBox {
int data;
EBox* firstedge; // 指向第一条依附该顶点的边
} VexBox;
typedef struct {
VexBox adjmulist[MAXSIZE];
int vexnum, degenum; // 无向图的当前顶点数和边数
} AMLGraph;
3.图的遍历
3.1 深度优先搜索
3.1.1 深度优先搜索遍历的过程
- 深度优先搜索(Depth First Search, DFS):树的先序遍历的推广
- 过程
- 从图中某个顶点 v 出发,访问 v
- 找出刚访问过的顶点和第一个未被访问的邻接点,访问该顶点;以该顶点为新顶点,重复次步骤,直至刚访问过的顶点没有未被访问的邻接点为止
- 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点
- 重复第二、三步,直至图中所有顶点都被访问过
- 根据访问时的路径和结点可以构成深度优先生成树
3.2 广度优先搜索
3.2.1 广度优先搜索遍历的过程
- 广度优先搜索(Breadth First Search, BFS):类似于树的按层次遍历
- 过程
- 从图中某个顶点 v 出发,访问 v
- 依次访问 v 的各个未曾访问过的邻接点
- 分别从这些邻接点出发依次访问它们的邻接点,并使 ”先被访问的顶点的邻接点“ 先于 ”后被访问的顶点的邻接点” 被访问;重复此步骤,直至图中所有已被访问的顶点的邻接点都被访问到
- 根据访问时的路径和结点可以构成深度优先生成树
4. 图的应用
4.1 最小生成树
- 最小代价生成树(Minimum Cost Spanning Tree)(最小生成树):在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树
4.1.1 普利姆算法
-
过程
假设 N = { V ,E } 是连通网,TE 是 N 上最小生成树中边的集合
- U = { u_0 }(u_0 ∈ V),TE = {}
- 在所有 u ∈ U,v ∈ V-U 的边( u ,v )∈ E 中找一条权值最小的边 ( u_0 ,v_0 )并入集合 TE ,同时 v_0 并入 U
- 重复第二步,直至 U = V 为止
4.1.2 克鲁斯卡尔算法
-
过程
假设 N = { V ,E } 是连通网,将 N 中的边按权值从小到大的顺序排列
- 初始状态为只有 n 个顶点而无边的非连通图 T = (V,{}),图中每个顶点自成一个连通分量
- 在 E 中选择权值最小的边,若该边依附的顶点落在 T 中不同的连通分量上(不形成回路),则将此边加入到 T 中,否则舍去此边而选择下一条权值最小的边
- 重复第二步,直至 T 中所有顶点都在同一连通分量上为止
4.2 最短路径
- 在带权有向网中,习惯上称路径上的第一个顶点为源点(Source),最后一个顶点为终点(Destination)
4.2.1 从某个源点到其余各顶点的最短路径
-
迪杰斯特拉(Dijkstra) 算法:按路径长度递增的次序产生最短路径的算法
-
过程
-
对于网 N = { V ,E } ,将 N 中的顶点分成两组
- 第一组 S :已求出的最短路径的终点集合(初始时只包含源点 v_0 )
- 第二组 V-S :尚未求出的最短路径的顶点集合(初始时为 V-{v_0} )
-
按各顶点与 v_0 间最短路径长度递增的次序,逐个将集合 V-S 中的顶点加入到集合 S 中去;在这个过程中,始终保持从 v_0 到集合 S 中各顶点的路径长度始终不大于到集合 V-S 中各顶点的路径长度
-
4.2.2 每一对顶点之间的最短路径(不会)
- 弗洛伊德(Floyd) 算法:
4.3 拓扑排序
4.3.1 AOV-网
- 有向无环图(Directed Acycline Graph, DAG):无环的有向图
- 顶点表示活动的网(Activity On Vertex Network,AOV-网):用顶点表示活动,用弧表示活动间的优先关系的有向图
- 拓扑排序:将 AOV-网 中所有顶点排成一个线性序列
- 序列满足:若在 AOV-网 中由顶点 v_i 到顶点 v_j 有一条路径,则在该线性序列中的顶点 v_i 必定在顶点 v_j 之前
4.3.2 拓扑排序的过程
- 在有向图中选一个无前驱的顶点且输出它
- 从图中删除该顶点和所有以它为尾的弧
- 重复第一、二步,直至不存在无前驱的顶点
- 若此时输出的顶点数小于有向图的顶点数,则说明有向图中存在环,否则输出的顶点序列即为一个拓扑排序
4.4 关键路径
4.4.1 AOE-网
-
边表示活动的网(Activity On Edge Network,AOV-网):用边表示活动,带权的有向无环图
- 顶点:事件
- 弧:活动
- 权:活动持续的事件
-
源点:网中只有一个入度为 0 的点
-
汇点:网中只有一个出度为 0 的点
-
带权路径长度(路径长度):一条路径各弧上的权值之和
-
关键路径(Critical Path):一条从源点到汇点的带权路径长度最长得到路径
-
关键活动:关键路径上的活动
4.4.2 关键路径序的求解过程
- 对图中顶点进行排序,在排序过程中按拓扑排序求出每个事件的最早发生时间 ve(i)
- 按拓扑排序求出每个事件的最晚发生时间 vl(i)
- 求出每个活动 a_i 的最早开始时间 e(i)
- 求出每个活动 a_i 的最晚开始时间 l(i)
- 找出 e(i) = l(i) 的活动 a_i,即为关键活动;由关键活动形成的由源点到汇点的每一条路径就是关键路径,关键路径可能不止一条