概念
图: G=(V, E)|| Graph=(Vertex, Edge) V:顶点(数据元素)的有穷非空集合 E:边的有穷集合
无向图: 每条边都是没有方向的
有向图: 每条边都是有方向的
完全图: 任意两个点都有一条边相连
- 无向完全图,n 个顶点,有 n(n-1)/2 条边
- 有向完全图,n 个顶点,有 n(n-1) 条边
稀疏图: 有很少的边或弧的图 (e<nlogn)
稠密图: 有较多边或弧的图
网: 边/弧带权的图
邻接: 有边/弧相连的两个顶点之间的关系 存在 (vi , vj ),则称 vi 和 vj 互为邻接点 存在 <vi , vj >,则称 vi 邻接到 vj , vj 邻接于 vi
关联(依附): 边/弧与顶点之间的关系 存在 (vi , vj ) 或者 <vi , vj >,则称该边/弧关联于vi 和 vj
顶点的度: 与该顶底相关联的边的数目,记为 TD(v) 在有向图中,顶点的度等于该顶点的 入度 与 出度 之和 顶点 v 的入度是以 v 为终点的有向边的条数,记作 ID(v) 顶点 v 的出度是以 v 为起始点的有向边的条数,记作 OD(v)
问:当有向树中仅有 1 个顶点的入度为 0,其余顶点的入度均为 1,此时是什么形状
树,而且是一棵有向树
路径: 接续的边构成的顶点序列
路径长度: 路径上边或弧的数目/权值之和
回路(环): 第一个顶点和最后一个顶点相同的路径
简短路径: 除路径起点和终点可以相同外,其余顶点均不相同的路径
简单路径(简单环): 除路径起点和终点相同外,其余顶点均不相同的路径
连通图(强连通图): 在无(有)向图 G=(V, {E}) 中,若对任何两个顶点 v、u 都存在从 v 到 u 的路径,则称 G 是连通图(强连通图)
权与网: 图中边或弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或耗费 带权的图称为 网
子图: 设有两个图 G=(V, {E} )、G1(V1, {E1} ),若 V1⊆V,E1⊆E,则称 G1 是 G 的子图
连通分量(强连通分量):
- 无向图 G 的 极大连通子图 称为 G 的连通分量 极大连通子图的意思是:该子图是 G 的连通子图,将 G 的任何不在该子图中的顶点加入,子图不再连通
- 有向图 G 的 极大连通子图 称为 G 的强连通分量 极大强连通子图的意思是:该子图是 G 的强连通子图,将 G 的任何不在该子图中的顶点加入,子图不再强连通
极小连通子图: 该子图是 G 的连通子图,在该子图中删除任何一条边,子图不再连通
生成树: 包含无向图 G 所有顶点的极小连通子图
生成森林: 对非连通图,由各个连通分量组成的生成树的集合
类型定义
ADT Graph{
数据对象V: 具有相同特性的数据元素的集合,称为顶点集;
数据关系R: R={VR}
VR = {
<v,w> | <v,w> | v,w∈V ∧ p(v,w),
<v,w> 表示从 v 到 w 的弧;
P(v,w) 定义了弧 <v,w> 的信息;
}
基本操作P:
Create_Graph(); //创建图
// 初始条件:无
// 操作结果:生成一个没有顶点的空图 G
GetVex(G,v); // 求图中顶点 v 的值
// 初始条件:图 G 存在,v 是图中的一个顶点
// 操作结果:生成一个没有顶点的空图 G ?? 应该是返回顶点的值吧
CreateGraph(&G, V, VR);
// 初始条件:V 是图的顶点集,VR 是图中弧的集合
// 操作结果:按 V 和 VR 的定义构造图 G
DFSTraverse(G);
// 初始条件:图 G 存在
// 操作结果:对图进行深度优先遍历
BFSTraverse(G);
// 初始条件:图 G 存在
// 操作结果:对图进行广度优先遍历
}ADT Graph;
图的存储结构
图的逻辑结构: 多对多
图没有顺序存储结构,但可以借助二维数组来表示元素间的关系;
数组表示法(邻接矩阵)
链式存储结构:邻接表、临界多重表、十字链表
邻接矩阵
无向图
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
-
设图 A = (V,E) 有 n 个顶点,则 定点表Vexs[n]
i 0 1 2 ... n-1 Vexs[i] v1 v2 v3 ... vn -
图的邻接矩阵是一个二维数组
A.arcs[n][n]
,定义为:
特点:
- 对角线上都为 0
- 矩阵为对称矩阵
- 顶点 i 的度 = 第 i 行(列)中 1 的个数
- 完全图的邻接矩阵中,对角元素为 0 ,其余为 1
有向图
从行看与从列看有不同的含义
特点:
- 有向图的邻接矩阵很可能不对称
- 顶点的出度 = 第 i 行元素之和
- 顶点的入度 = 第 i 列元素之和
- 顶点的度 = 第 i 行元素之和 + 第 i 列元素之和
网
网(有权图)的邻接矩阵表示法
存储表示
邻接矩阵的存储表示:用 两个数组 分别存储 顶点表 和 邻接矩阵
#define MaxInt 32767 // 表示极大值,即∞
#define MVNum 100 // 最大顶点数
typedef char VerTextType; // 设顶点的数据类型为字符型
typedef int ArcType; // 假设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum]; // 顶点表
ArcType arcs[MVNum][MVNum]; // 邻接矩阵
int vexnum, arcnum; // 图的当前点数和边数
}AMGraph; // Adjacency Matrix Graph
创建无向网
算法思想:
- 输入总定点数和总边数
- 依次输入点的信息存入顶点表中
- 初始化邻接矩阵,使每个权值初始化为极大值
- 构造邻接矩阵
Status CreateUDN(AMGraph &G){
scanf("%d%d",&G.vexnum,&G.arcnum); // 输入总顶点数,总边数
for(i=0; i<G.vexnum; ++i)
scanf("%c", &G.vexs[i]); // 依次输入点的信息
for(i=0; i<G.vexnum; i++) // 初始化邻接矩阵
for(j=0; j<G.vexnum; j++)
G.arcs[i][j] = MaxInt; // 边的权值均置为极大值
for(k=0; k<G.arcnum; k++) // 构造邻接矩阵
{
scanf("%d%d%d", &v1,v2,v3); // 输入一条边所依附的顶点及边的权值
i = LocateVex(G, v1);
j = LocateVex(G, v2);
// 确定 v1 和 v2 在 G 中的位置
G.arcs[i][j] = w; // 边 <v1, v2> 的权值置为 w
G.arcs[j][i] = G.arcs[i][j]; //置 <v1, v2> 的对称边 <v2, v1> 的权值为 w
}
return OK;
} // CreateUDN
int LocateVex(AMGraph G, VertexType u){
int i;
for(i=0; i<G.vexnum; ++i)
if(u == G.vexs[i])
return i;
return -1;
}
创建无向图
- 初始化邻接矩阵时,w 均为0
- 构造邻接矩阵时,w 为1
创建有向网
- 邻接矩阵是非对称矩阵
- 仅为
G.arcs[i][j]
赋值,无需为G.arcs[j][i]
赋值
创建有向图
做无向图与有向网的修改
优点
-
直观、简单、好理解
-
方便检查任意一对顶点间是否存在边
-
方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
-
方便计算任一顶点的度
- 行的和为入度
- 列的和为出度
无向图:对应行(或列)非0元素的个数
有向图:对应行非0元素的个数是“出度”,对应列非0元素的个数是入度
缺点
- 不便于增加和删除结点
- 浪费空间——存储稀疏图(点多边少)有大量无效元素
- 空间复杂度O( n2)
- 对稠密图(特别是完全图)很合算
- 浪费时间——统计稀疏图中有多少条边
邻接表
链式表示法
无向图
特点:
- 邻接表不唯一
- 若无向图中有 n 个顶点,e 条边,则其邻接表需要 n 个头结点和 2e 个表结点。适合存储稀疏图
- 无向图中顶点 vi 的度为第 i 个单链表中的结点数
有向图
特点: 找出度易,找入度难
- 顶点 vi 的出度为第 i 个单链表中的结点个数
- 顶点 vi 的入度为整个单链表中邻接点域值是 i-1 的结点个数
或者采用 “逆邻接表”
图的邻接表存储表示
- 顶点的结点结构
data | firstarc |
---|
typedef VNode{
VerTexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; // AdjList 表示邻接表类型
- 弧(边)的结点结构
adjvex | nextarc | info |
---|
#define MVNum 100 // 最大顶点数
typedef struct ArcNode{ // 边结点
int adjvex; // 该边所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条边的指针
OtherInfo info; // 和边相关的信息
}ArcNode;
- 图的结构定义
typedef struct{
AdjList vertices; // vertices 是 vertex 的复数
int vexnum, arcnum; // 图的当前顶点数和弧数
}ALGraph;
-
邻接表操作举例
ALGragh G; // 定义了邻接表表示的图G
G.vexum=5;G.arcnum=5; // 图G包含了5个顶点5条边
G.certices[1].data='b'; // 图G中第2个顶点是b
p=G.vertices[1].firstarc; // 指针p指向顶点b的第一条边结点
p->adjvex=4; // p指针所指边界点是到下标为4的结点的边
邻接表法创建无向网
算法思想
-
输入总顶点数和总边数
-
建立顶点表
依次输入点的信息存入顶点表中
使每个表头结点的指针域初始化为NULL
-
创建邻接表
依次输入每条边依附的两个顶点
确定两个顶点的序号 i 和 j ,建立边结点
将此边结点分别插入到 vi 和 vj 对应的两个边链表的头部
Status CreateUDG(ALGraph &G){ //邻接表表示法创建无向图G scanf("%d%d",&G.vexnum,&G.arcnum); //输入总顶点数与总边数 for(i=0;i<G.vexnum;i++){ scanf("%d",G.vertices[i].data); //输入顶点值 G.vertices[i].firstarc=NULL; //初始化表头结点指针域 } for(k=0;k<G.arcnum;++k){ //输入各边,构造邻接表 scanf("%d%d",&v1,&v2); //输入一条边依附的两个顶点 i=LocateVex(G,v1); //LocateVex 查找函数 j=LocateVex(G.v2); p1=(ArcNode*)malloc(sizeof(ArcNode)); //生成新的边结点p1 p1->adjvex=j; // 邻接点序号为j p1->nextarc=G.vertices[i].firstarc; //头插法 G.vertices[i].firstarc=p1; //将新结点*p1插入顶点vi的边表头部 p2=(ArcNode*)malloc(sizeof(ArcNode)); //生成另一个对称的新结点*p2 p2->adjvex=i; // 邻接点序号为i p2->nextarc=G.vertices[j].firstarc; G.vertices[j].firstarc=p2; } return OK; }//CreateUDG
优点
- 方便找任一顶点的邻接点
- 存储稀疏图节省空间
- 需要N个头指针+2E个结点(每个结点至少2个域)
- 顶点的度
- 对无向图,方便计算顶点的度
- 对有向图,方便计算出度,入度不便计算;构造“逆邻接表”方便计算入度,不便计算出度
缺点
- 不方便检查任一对顶点间是否存在边
邻接矩阵邻接表关系
- 联系 邻接表中每个链表对应于邻接矩阵中的一行链表中结点个数等于非零元素的个数
- 区别
- 对于任一确定的无向图,邻接矩阵唯一,但邻接表不唯一
- 邻接矩阵空间复杂度O(n2),邻接表空间复杂度O(n+e)
- 用途 邻接矩阵多用于稠密图,邻接表多用于稀疏图
十字链表
主要存储有向图
解决邻接表存储有向图时求结点的度困难的问题
- 顶点结点
data | firstin | firstout |
---|
- 弧结点
tailvex | headvex | hlink | tlink |
---|
邻接多重表
解决邻接表存储无向图时每条边都要存储两边的问题
邻接表,在删除一条边时需要找表示此边的两个结点,不方便
-
顶点结点
Data firstedge -
边结点
mark ivex vex jlink info
图的遍历
访问图中所有结点,并且每个结点只访问一次
-
避免重复访问的方法?
设置 辅助数组 visited[n], 用来标记每个被访问过的顶点
- 初始状态
visited[i]=0
- 被访问之后
visited[i]=1
- 初始状态
遍历——找每个顶点的邻接点的过程
深度优先遍历 DFS
Depth_First Search——DFS
算法思想
- 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点 w1
- 从 w1 出发,访问与w1 邻接但还未被访问过的顶点w2
- 再从w2 出发,进行类似的访问
- 直至到达所有邻接顶点都被访问过的顶点 u 为止
- 接着,退回一步,退到前一次访问过的顶点,看看是否还有其它没有被访问的邻接顶点
- 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问过程
- 如果没有,就再退回一步进行搜索,重复上述过程,直至连通图中所有顶点都被访问过为止
连通图的深度优先遍历类似于树的先根遍历
算法实现
void DFS(AMGraph G, int v){ // 图G为邻接矩阵类型
printf("%d",v);
visited[v]=true; // 访问第v个顶点
for((G.arcs[v][w]!=0)&&(!visited[w]))
DFS(G,W);
// w 是 v 的邻接点,如果w未访问,则递归调用DFS
}
算法效率
- 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)
- 用邻接表来表示图,虽然有2e个表结点,但只需要扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
结论:
1.稠密图适合在邻接矩阵上进行深度遍历
2.稀疏图适合在邻接表上进行深度遍历
广度优先遍历 BFS
Breadth_First Search
算法思想
- 从图的某一结点出发,依次访问该结点的所有邻接顶点
- 按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点
- 重复以上过程,直至所有顶点均被访问
void BFS(Graph G,int v){ // 广度优先非递归遍历连通图G
printf("%d",v);
visited[v]=true; // 访问第v个顶点
InitQueue(Q); // 辅助队列Q初始化,置空
EnQueue(Q,v); // v进队
while(!QueueEmpty(Q)){ // 队列非空
for(w=FirstAdjVex(G,u); w>=0; w=NextAdjVex(G,u,w))
if(!visited[w]){
printf("%d",w);
visited[w]=true;
EnQueue(Q,w);
}
}
}
算法效率
- 邻接矩阵,BFS对于每一个个访问到的顶点,都要循环检测矩阵中的一整行(n个元素),总的时间代价为O(n2)
- 邻接表,有2e个表结点,但只需要扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
DFS与BFS算法效率
- 空间复杂度相同,都是O(n),借用了堆栈或队列
- 时间复杂度只与存储结构有关(邻接矩阵,邻接表),与搜索路径无关
图的应用
最小生成树
生成树: 所有顶点均由边连接在一起,但不存在回路的图
- 一个图可以有许多棵不同的生成树
- 所有生成树具有以下特点:
- 生成树的顶点个数与图的顶点个数相同;
- 生成树是图的极小连通子图,去掉一条边则非连通;
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径唯一
- 含n个顶点,n-1条边的图不一定是生成树
定义
给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树
构造最小生成树
Minimun Spanning Tree
构造最小生成树算法很多,其中大多利用了 MST
的性质
MST性质
设 N=(V,E) 是一个联通网,U 是顶点集 V 的一个非空子集。
若边 (u,v) 是一条具有最小权值的边,其中 u∈U, v∈V-U, 则必存在一棵包含边 (u,v) 的最小生成树
解释
在生成树构造过程中,图中 n 个顶点分属两个集合:
- 已落在生成树上的顶点集: U
- 尚未落在生成树上的顶点集: V-U
接下来则应在所有连通 U 中顶点和 V-U 中顶点的边中选取权值最小的边
普里姆算法
Prim
算法思想
克鲁斯卡尔算法
Kruskal
最小生成树可能不唯一
两种算法比较
算法名 | 普里姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n2) n为顶点数 | O(eloge) e为边数 |
适应范围 | 稠密图 | 稀疏图 |
最短路径
最短路径是指:在有向网中 A(源点) 点到达 B(终点) 点 的多条路径中,寻找一条各边权值之和最小的路径
最短路径与最小生成树不同,路径上不一定包含 n 个顶点,不一定包含 n-1 条边
两类问题
两点间最短路径
称 单源最短路径 ,用 Dijkstra(迪杰斯特拉) 算法
Dijkstra算法
按路径长度递增次序产生最短路径
算法思想:
- 初始化:先找出从源点 v0 到各终点 vk 的直达路径 (v0 ,vk ),即通过一条弧到达的路径
- 选择:从这些路径中找出一条长度最短的路径 (v0 ,u)
- 更新:对其余各条路径进行适当调整: 若在图中存在弧 (u,vk ),且 (v0 ,u) + (u,vk ) < (v0 ,k),则以路径 (v0 ,u,vk ) 代替 (v0 ,k) 在调整后的各条路径中,再找长度最短的路径,以此类推
某源点到其他各点最短路径
称 所有顶点间的最短路径 ,用 Floyd(弗洛伊德) 算法
算法思想:
- 逐个顶点试探
- 从 vi 到 vj 的所有可能存在的路径
- 选出最短的一条路径
有向无环图及其应用
有向无环图简称 DAG 图(Directed Acycline Graph)
AOV 网
拓扑排序
用一个有向图表示一个工程的各个子工程及其相互制约关系,其中以顶点表示活动,弧表示各活动之间的有限制约关系,称这种有向图为 顶点表示活动的网 (Activity On Vertex network)
- AOV 网中,不允许有回路
如何判断 AOV 网中是否有回路?
使用拓扑排序
对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该 AOV 网必定不存在环
拓扑排序
在 AOV 网没有回路的前提下,我们将全部活动排列称一个线性序列,使得若 AOV 网中有弧 <i,j> 存在,则在这个序列中,i一定排在j前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序
方法
- 在有向图中选择一个没有前驱的顶点且输出
- 从图中删除该顶点和所有以它为尾的弧
- 重复上述两步,直至全部顶点均输出,或者当图中不存在无前驱的顶点为止
AOE 网
关键路径: 源点到汇点路径长度最长的路径
用一个有向图表示一个工程的各个子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件。称这种有向图为 边表示活动的网 (Activity On Edge)
源点: 入度为 0 的顶点
汇点: 出度为0的顶点
求解关键路径时用到的4个参量
ve(vk) —— 事件 vk 的最早发生时间
vl(vk) —— 事件 vk 的最迟发生时间
e(ai) —— 活动 ai 的最早开始时间
l(ai) —— 活动 ai 的最迟开始时间
活动完成的时间余量:
一个活动 ai 的最迟开始时间 l(i) 和其最早开始时间 e(i) 的差额 d(i) = l(i) - e(i)
有些活动没有时间余量
求关键路径步骤:
- 求 ve(i)、vl(j)
- 求 e(i)、l(i)
- 计算 l(i) - e(i)
1. e(i) = ve(j)
2. l(i) = vl(k) - w<sub>j,k</sub>
关键路径注意
- 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素。因此可以通过加快关键活动来缩短整个工期的工程。
- 不能任意缩短关键活动,因为一旦缩短到一定程度,该关键活动会变成非关键活动。
- 网中的关键路径可能并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快包括在所有关键路径上的关键活动才能达到缩短工期的目的