图
定义及术语
图:G = (V,E)
V:顶点(数据元素)的有穷非空集合**(一定要有)**
E:边的有穷集合**(不一定要有)**
基本术语:
- 有向图、无向图
- 完全图:任意两点都有一条边相连
- 稀疏图:有很少边或弧的图(e<nlogn)
- 稠密图:有较多边或弧的图
- 网:边/弧带权的图
- 邻接:有边/弧相连的两个顶点之间的关系
存在(vi, vj),则称vi和vj互为邻接点
存在<vi,vj>,则称vi邻接到vj,vj邻接于vj - 关联(依附):边/弧与顶点之间的关系
存在(vi,vj)/<vi,vj>,则称该边/弧关联于vi和vj - 顶点的度:与该顶点相关联的边的数目,记为TD(v)
区别:树的结点的度:拥有的子树数目
在有向图中,顶点的度等于该顶点的入度与出度之和
顶点v的入度:以v为终点的有向边的条数,称为ID(v)
顶点v的出度:以v为始点的有向边的条数,称为OD(v) - 路径:接续的边构成的顶点序列
- 路径长度:路径上边或弧的数目/权值之和
- 回路(环):第一个顶点和最后一个顶点相同的路径
- 简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
- 简单回路(环):除路径起点和终点一定相同外,其余顶点均不相同的路径
- 连通图(强连通图):从无(有)向图G = (V,{E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)
- 权和网:图中边或弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或耗费,带权的图称为网
- 子图:
- 连通分量(强连通分量):
①无向图G的极大连通子图称为G的连通分量
极大连通子图的意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入
,子图不在连通
②有向图G的极大强连通子图称为G的强连通分量
极大强连通子图的意思是:该子图是G强连通子图,将G的任何不在该子图中的顶点加入,子图不在强连通
- 极小连通子图:该子图是G的连通子图,在该子图中
删除任何一条边
(与极大连通子图相比较),子图不再连通 - 生成树:包含无向图G所有顶点的极小连通子图
- 生成森林:对非连通图,由各个连通分量的生成树的集合
案例引入
六度空间理论
图的基本操作
1.Create_Graph():图的创建操作
初始条件:无
操作结果:生成一个没有顶点的空图G
2. GetVex(G,v):求图中的顶点v的值
初始条件:图G存在,v是图中的一个顶点
操作结果:生成一个没有顶点的空图G
3. CreateGraph(&G,V,VR)
初始条件:V是图中的顶点集,VR是图中弧的集合
操作结果:按V和VR的定义构造图G
4. GFSTraverse(G)
初始条件:图G存在
操作结果:对图进行深度优先遍历
5. BFSTraverse(G)
初始条件:图G存在
操作结果:对图进行广度优先遍历
图的存储结构
图的逻辑结构:多对多
- 图没有顺序存储结构,但是可以借助二维数组来表示元素间的关系,称为
数组表示法(邻接矩阵)
- 链式存储结构:多重链表(邻接表、邻接多重表,十字链表)
数组(邻接矩阵)表示法
无向图
建立一个顶点集(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
设图A = (V,E)有n个结点,则
图的邻接矩阵是一个二维数组A.arcs[n][n],定义为
有向图
网(即有权图)
类型定义
#define MaxInt 32767 //表示极大值,即∞
#define MVNum 100 //最大顶点数
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct {
VerTexType vexs[MVNUM]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum, arcnum; //图的当前点数和边数
}AMGraph; //Adjacency Matrix Graph
创建无向网
可利用无向网稍作改动来表示 无向图、有向图、有向网
//用邻接矩阵表示法创建无向网
Status CreateUDN(AMGraph &G){
cin >> G.vexnum >> G.arcnum; //输入点数、边数
for(i = 0; i < G.vexnum; i++) //输入点的信息
cin >> G.vexs[i];
for(i = 0; i < G.vexnum; i++) //初始化邻接矩阵
for(j = 0; j < G.vexnum; j++)
G.arcs[i][j] = MAXlnt;
for(k = 0; k < G.arcnum; k++) //构建邻接矩阵
{
cin >> v1 >> v2 >> w; //输入一条边依附的顶点和边的权值
i = LocateVex(G, v1); //确定v1在G中的位置
j = LocateVEX(G, v2); //确定v2在G中的位置
G.arcs[i][j] = w; //邻接矩阵赋值
G.arcs[j][i] = G.arcs[i][j]; //无向图,反过来值相同
}
return OK;
}
int LocateVex(ALGraphy G, string u) {
//在图G中查找顶点u,找到则返回u在顶点表中的下标,未找到返回-1
int i;
for (i = 0; i < G.vexnum; i++)
if (u == G.vertices[i].data) return i;
return -1;
}
优缺点
优点:简单易懂、便于知道结点的临界点、了解边的存在与否、结点的“度”
缺点:不利于删除和增加结点、浪费空间——存在稀疏图(点多边少)有大量无效元素、浪费时间——统计稀疏图一共多少条边
邻接表
顶点:按编号顺序将顶点数据存储在一维数组中;
关联同一顶点的边(以顶点为尾的弧):用线性链表存储
若是处理网:表结点的域再加info域
无向图
特点:
- 邻接表不唯一
- 若无向图中有n个顶点,e条边,则其邻接表需n个头结点和2e个表结点,适宜存储稀疏图
o(n+2e) < O(n^2) - 无向图中顶点vi的度为第i个单链表中的表结点数
有向图
类型定义
//顶点的结点结构
typedef struct VNode {
VerTexTypde data; //顶点信息
ArcNode * firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型
例如:AdjList v; 相当于VNode v[MVNum]
//弧(边)的结点结构
#define MVNum 100 //最大顶点数
typedef struct ArcNode { //边结点
int adjvex; //该边所指向的顶点的位置
struct ArcNode * nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
//图的结构定义
typedef struct {
AdjList vertices; //vertices--vertex的复数
int vexnum, arcnum; //图的当前顶点数和弧数
}ALGraph;
采用邻接表表示法创建无向网
算法思想:
- 输入总顶点数和总边数
- 建立顶点表
依次输入点的信息存入顶点表中
使每个表头结点的指针域初始化为NULL - 创建邻接表
依次输入每条边依附的两个顶点
确定两个顶点的序号i和j,建立边结点
将此边结点分别插入vi和vj对应的两个边链表的头部
代码实现:
Status CreateUDG (ALGraph &G) { //采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for (i = 0; i < G.vexnum; i++) { //输入各点,构造表头结点
cin >> G.vertices[i].data; //输入顶点值
G.vertices[i].firstarc = NULL; //初始化表头结点的指针域
}
for (k = 0; k < G.arcnum; k++) { //输入各边,构造邻接表
cin >> v1 >> v2; //输入一条边依附的两个顶点
i = LocateVex(G, v1);
j = LocateVex(G, v2);
p1 = new ArcNode ; //生成一个新的边结点*p1
p1->adjvex = j; //邻接点序号为j
//使用头插法将新结点插入表结点的链表中
p1->nextarc = G.vertices[i].firstarc;
G.vertices[i].firstarc = p1; //将新结点*p1插入顶点vi的边表头部
p2 = new ArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex = i; //邻接点序号为i
//使用头插法将新结点插入表结点的边链表中
p2->nextarc = G.vertices[j].firstarc;
G.vertices[j].firstarc = p2; //将新结点*p2插入顶点vj的边表头部
}
return OK;
}//CreateUDG
邻接表特点
- 方便找任一顶点的所有“邻接点”
- 节约稀疏图的空间:需要N的头指针+2E个结点(每个结点至少2个域)
- 方便计算任一结点的“度”?
- 对于无向图:是的
- 对于有向图:只能计算“出度”;需要构造“逆邻接表”(存指向自己的边)来方便计算“入度”
- 不方便计算任意一对顶点间是否存在边
邻接表和邻接矩阵的关系
- 联系
邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数 - 区别
①对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链表次序与顶点编号无关)。
②邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e) - 用途:邻接矩阵多用于稠密图,而邻接表多用于稀疏图
十字链表(有向图)
十字链表:存储有向图,可以看成是邻接表和逆邻接表的结合
有向图的每一条弧对应十字链表中的一个弧结点,有向图的每个顶点对应十字链表中的结点,叫做顶点结点
headvex:以该结点为弧头的弧;tailvex:以该结点为弧尾的弧
hlink:以该结点为弧头的下一条弧;tlink:以该结点为弧尾的下一条弧
邻接多重表(无向图)
图的遍历
- 遍历定义:从已给的连通图中某一顶点出发,沿着一些边访问图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它就是图的基本运算
- 遍历实质:找每个顶点的邻接点的过程
- 图的特点:图中可能存在回路,且图的任一顶点都可能与其它顶点相遇,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点
- 如何避免重复访问?
解决思路:设置辅助数组visited[n],用来标记每个被访问过的顶点
①初始状态visited[i]为0
②顶点i被访问,改visited[i]为1,防止被多次访问 - 常用的遍历
①深度优先搜索(DFS)
②广度优先搜索(BFS)
深度优先搜索(DFS)
邻接矩阵上的遍历
void DFS (AMGraph G, int v) { //图G为邻接矩阵类型
cout << v; visited[v] = true; //访问第v个顶点
for(w = 0; w < G.vexnum; w++) { //依次检查邻接矩阵v所在的行
if ((G.arcs[v][w] != 0) && (!visited[w]))
DFS(G,w);
//w是v的邻接点,如果w未访问,则递归调用DFS
}
}
算法效率分析
- 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在的行,时间复杂度为O(n^2)
- 用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
结论:
- 稠密图适合于在邻接矩阵上进行深度遍历
- 稀疏图适合于在邻接表上进行深度遍历
非连通图的遍历
广度优先搜索(BFS)
一层一层点亮所有的灯
非连通图的广度优先遍历
顶点访问次序:acdefhkbg
与树的层次遍历十分相似
算法效率分析
- 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行(n个元素),总的时间代价为O(n^2)
- 用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为O(n+e)
DFS与BFS的算法效率比较
- 空间复杂度相同,都是O(n)(借用了堆栈或队列)
- 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关
图的应用
最小生成树
基本知识
- 生成树:所有顶点均由边连接在一起,但不存在回路的图
- 一个图可以有许多棵不同的生成树
- 所有的生成树具有以下共同特点:
①生成树的顶点个数与图的顶点个数相同
②生成树是图的极小连通子图,去掉一条边则非连通
③一个有n个顶点的连通图的生成树有n-1条边
④在生成树中再添加一条边必然形成回路
⑤生成树中任意两个顶点间的路径是唯一的 - 含n个顶点n-1条边的图不一定是生成树
- 无向图的生成树的构造
- 最小生成树:给定一个无项网络(边带权值),在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树
- 用途·:建立通信网、建立公路
- 构造最小生成树(MST)
构造最小生成树的算法很多,其中多数算法都利用了MST的性质
MST性质:设N = {V,E}是一个连通网,U是顶点集V的一个非空子集,若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树 - MST性质解释:
- 在生成树的构造过程中,图中n个顶点分属两个集合:已在生成树上的顶点集U,尚未落在生成树上的顶点集V-U
- 接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边
构造最小生成树的算法1——Prim算法
算法思想:
- 设N=(V,E)是连通网,TE是N上最小生成树中边的集合
- 初始令U={u0},(u0∈V),TE={}.
- 令
所有
(并非需要上一个点末尾开始)的u∈U,v∈V-U的边(u,v)∈E中,找一条代价最小的边(u0,v0); 将(u0,v0)并入集合TE,同时v0并入U。- 重复上述步骤3,直到U=V为止,则T=(V,TE)为N的最小生成树
构造最小生成树的算法2——Kruskal算法
算法思想:
- 设连通网N=(V,E),令最小生成树初始状态为只有n个顶点而无边的非连通图T=(V,{}),每个顶点自成一个连通分量
- 在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
- 以此类推,直至T中所有的顶点都在同一连通分量上为止
最短路径
- 典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
交通网络用有向网来表示:
顶点——表示地点
弧——表示两个地点有路连通
弧上的权值——表示两地点之间的距离、交通费或途中所花费的时间等。
如何能够使一个地点到另一个地方的运输时间最短或运费最省?这就是一个求两个地点间的最短路径问题 - 问题抽象:在有向网中A点(源点)到B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。
- 最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
单源最短路径——用Dijkstra算法
求两点间的最短路径
算法思想:
- 初始化:先找出从源点v0到各终点vk的直达路径(v0,vk),即通过一条弧到达的路径
- 选择:从这些路径中找出一条长度最短的路径(v0,u)
- 更新:然后对其余各条路径进行适当调整:
- 若图中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),则以路径(v0,u,vk)代替(v0,vk)
在调整后的各条路径中,再找长度最短的路径,以此类推
所有顶点间的最短路径——用Floyd算法
求某源点到其他各点的最短路径
- Dijkstra算法;
- Floyd算法
算法思想:
- 逐个顶点试探
- 从vi到vj的所有可能存在的路径中
- 选出一条长度最短的路径
引入:
有向无环图:无环的有向图,简称DAG图
有向无环图常用来描述一个工程或系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)
一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成
- AOV网(拓扑排序)
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网- AOE网(关键路径)
用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称AOE网
拓扑排序
实例:排课表
- 定义:在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序
- 检测AOV网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必不存在环
关键路径
- 源点:入度为0的结点
- 汇点:出度为0的结点
- 关键路径——路径长度最长的路径
- 路径长度——路径上各活动持续时间之和
- 如何确定关键路径,需要定义4个描述量:
- ve(vj)——表示事件vj的最早发生时间
- vl(vj)——表示事件vj的最迟发生时间
- e(i)——表示活动ai的最早开始时间
- l(i)——表示活动ai的最迟开始时间
- l(i) - e(i)——表示完成活动ai的时间余量- 关键活动——关键路径上的活动,即l(i)==e(i)的活动
ve(vj)、vl(vj)、e(i)、l(i)的表示:
求关键路径步骤: