图
图
图的概念
G=(V,E)/Graph=(Vertex,Edge)
-
V:顶点(数据元素)的有穷非空集合
-
E: 边的有穷集合
-
无向图:每条边都是无方向的
-
有向图:每条边都是有方向的
-
完全图:任意两个点都有一条边相连
-
有向完全图<vi,vj>:n个顶点,n(n-1)条边
-
无向完全图(vi,vj):n个顶点,n(n-1)/2条边
-
-
稀疏图:有很少边或弧的图(e<nlogn)
-
稠密图:有较多边或弧的图
-
网:边/弧带权的图
-
权:图中边或弧所具有的相关数,表明从一个顶点到另一个顶点的距离或耗费
-
邻接:有边/弧相连的两个顶点的关系
-
(vi,vj),称vi、vj互为邻接点
-
<vi,vj>,称vi邻接到vj或vj邻接于vi
-
-
关联(依附):边/弧与顶点之间的关系 :(vi,vj)或 <vi,vj>称该边/弧关联于vi和vj
-
顶点的度:与该顶点相关联的边的数目,记为TD(V)
在有向图中分为入度ID(V)和出度OD(V)
当有向图中仅1个顶点的入度为0,其余顶点入度为1,此时是一棵有向树
-
路径:连续的边构成的顶点序列
-
路径长度:路径上边或弧的数目/权值之和
-
回路(环):第一个顶点和最后一个顶点相同的路径
-
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
-
简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径
连通图:
在无向图G=(V,{E})中,若对任意两个顶点v,u都存在从v到u的路径,则称G是连通图
在有向图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的连通子图且在该子图中任意删除一条边,子图不再联通
生成树:
-
连通且不存在回路
-
是图的极小连通子图(再加一条会形成回路)
-
一个有n个顶点的连通图生成树有n-1条边(反过来不一定)
-
任意两点间的路径是唯一的
生成森林:对非连通图,各个连通分量生成树的集合
图的抽象数据类型定义
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)∶
初始条件:图G存在,v是图中的一个顶点
操作结果:生成一个没有顶点的空图G
CreateGraph(&G,V,VR)
初始条件:V是图的顶点集,VR是图中弧的集合
操作结果:按V和VR的定义构造图G
DFSTraverse(G)
初始条件:图G存在
操作结果:对图进行深度优先遍历
BFSTraverse(G)
初始条件:图G存在
操作结果:对图进行广度优先遍历
} ADT Graph
图的存储结构
图的逻辑结构:多对多
图没有顺序存储结构,但可以使用二维数组表示元素间的关系——邻接矩阵/数组表示法
链式存储结构:
-
邻接表
-
邻接多重表
-
十字链表
邻接矩阵
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
A=(V,E)
-
顶点表:
-
图的邻接矩阵是一个二维数组
邻接矩阵的优点:直观、简单、好理解、方便查找邻接点、计算度
邻接矩阵的缺点:不便于增删顶点、浪费时/空间(稀疏图)
-
无向图的邻接矩阵:A.arcs[i][j]=1(两顶点间有边)/0(两顶点间没边)
- 无向图的邻接矩阵是对称矩阵,且对角线上元素都是0
- 顶点i的度=第i行(列)中1的个数
- 完全图的邻接矩阵,对角线元素都是0,其余都是1
-
有向图的邻接矩阵:
- 第i行:以结点vi为尾的弧/出度边;第i列:以结点vi为头的弧/入度边
- 有向图的邻接矩阵可能不是对称的
- 顶点的出度=第i行元素的和;顶点的入度=第i列元素的和
- 顶点的度=出度+入度即第i行元素之和+第i列元素之和
-
网(有权图)的邻接矩阵:A.arcs[i][j]=Wij(两顶点间有边)/∞(两顶点间没边)
-
邻接矩阵的定义
#define MVNum 100 //表示最大顶点数
#define MaxInt 32716 //表示极大值即∞
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //设边的权值类型为整型
typedef struct {
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph;
构建无向网
#define MVNum 100 //表示最大顶点数
#define MaxInt 32716 //表示极大值即∞
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //设边的权值类型为整型
typedef struct {
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph;
int LocateVex(AMGraph *G,VerTexType v){
for (int i = 0; i <G->vexnum ; ++i) {
if(v==G->vexs[i])
return i;
}
}
int CreateUDN(AMGraph *G){
char v1,v2;
int w;
printf("Please input the vexnum and arcnum:\n");
scanf("%d,%d",&G->vexnum,&G->arcnum);
for (int i = 0; i <G->vexnum ; ++i) { //构造顶点表
fflush(stdin);
printf("Please input the vexs:\n");
scanf("%c",&G->vexs[i]);
}
for (int j = 0; j <G->vexnum ; ++j) { //初始化邻接矩阵
for (int k = 0; k <G->vexnum ; ++k) {
G->arcs[j][k]=MaxInt;
}
}
for (int l = 0; l <G->arcnum ; ++l) { //构造邻接矩阵
int m,n;
fflush(stdin);
printf("Please input the arcs:\n");
scanf("%c,%c,%d",&v1,&v2,&w);
m=LocateVex(G,v1);
n=LocateVex(G,v2);
G->arcs[m][n]=w;
G->arcs[n][m]=G->arcs[m][n];
}
return 1;
}
邻接表(链式)
无向图
顶点表:按编号顺序将顶点数据存储在一维数组中;
头结点:data(数据本身)+firstarc(第一个边的边结点地址)
表结点:adjvex(邻接点在表中的序号)+nextarc(指向下一个边结点) +(info权)
有几条边就有几条边结点
例:
-
邻接表不唯一(顺序可变)
-
若无向图有n个结点,e条边,则邻接表需要n个头结点,2e个表结点, 适宜存储稀疏图
-
无向图中顶点vi的度=第i个单链表的结点数
定义
//边结点
typedef struct Arcnode {
int adjvex; //邻接点在表中的序号
int info; //该边的权值
struct Arcnode* nextarc; //指向下一条边结点的指针
}ArcNode;
//头结点
typedef struct Vnode{
char data;
ArcNode *firstarc;
}VNode,AdjList[MVNum];
//图
typedef struct {
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
创建无向图
#define MVNum 100 //表示最大顶点数
#include <stdlib.h>
//边结点
typedef struct Arcnode {
int adjvex; //邻接点在表中的序号
int info; //该边的权值
struct Arcnode* nextarc; //指向下一条边结点的指针
}ArcNode;
//头结点
typedef struct Vnode{
char data;
ArcNode *firstarc;
}VNode,AdjList[MVNum];
//图
typedef struct {
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
int LocateVex(ALGraph *G,char v){
for (int i = 0; i <G->vexnum ; ++i) {
if(v==G->vertices[i].data) return i;
}
}
int CreateUDG(ALGraph *G){
char v1,v2;
int m,n;
printf("Please input the vexnum and arcnum:\n");
scanf("%d,%d",&G->vexnum,&G->arcnum);
for (int i = 0; i <G->vexnum ; ++i) { //构造表头结点表
fflush(stdin);
printf("Please input the data:\n");
scanf("%c",&G->vertices[i].data);
G->vertices[i].firstarc=NULL; //初始化表头结点指针域
}
for (int j = 0; j <G->arcnum ; ++j) { //构造邻接表
fflush(stdin);
printf("Please input the two vertices where an edge is located\n");
scanf("%c,%c",&v1,&v2);
m=LocateVex(G,v1);
n=LocateVex(G,v2);
ArcNode *p1=(ArcNode*)malloc(sizeof(ArcNode)); //建立出度边
p1->adjvex=n;
p1->nextarc=G->vertices[m].firstarc;
G->vertices[m].firstarc=p1;
// ArcNode *p2=(ArcNode*)malloc(sizeof(ArcNode)); //建立入度边
// p1->adjvex=m;
// p1->nextarc=G->vertices[n].firstarc;
// G->vertices[n].firstarc=p2;
}
return 1;
}
有向图
每个顶点的单链表只记录出度所在边结点
-
有向图中顶点vi的出度=第i个单链表的结点数
-
顶点vi的入度为整个单链表中邻接域值是i-1的结点个数(遍历整个邻接表)
找出度易,找入度难
逆邻接表
每个顶点的单链表只记录入度所在边结点
-
有向图中顶点vi的入度=第i个单链表的结点数
-
顶点vi的出度为整个单链表中邻接域值是i-1的结点个数(遍历整个邻接表)
找入度易,找出度难
邻接表特点
当邻接表的存储结构形成后,图便唯一确定
-
方便找任一顶点的所有邻接点
-
节约稀疏图空间,只需N+2E个结点
-
无向图方便计算顶点的度
-
但不方便检查任意一对顶点是否存在边
邻接矩阵与邻接表的联系
-
邻接表中每个链表对应与邻接矩阵中的一行,链表中结点个数=一行中非零元素个数
-
对于任一确定无向图,邻接矩阵是唯一的,但邻接表不唯一
-
邻接矩阵空间复杂度O(n^2);邻接表的空间复杂度是O(n+e)
因此,邻接矩阵多用于稠密图,邻接表多用于稀疏图
十字链表
用于有向图,易求得结点的度
顶点结点结构:data+firstin+firstout
弧结点:tailvex+headvex+hlink+tlink
例:
邻接多重表
用于无向图,用于解决存储两遍问题
顶点结点:data+firstedge(第一条依附于该顶点的边)
边结点:mark(标志域:标记此边是否被搜索过)+ivex+ilink(指向依附于ivex的下一条边)+jvex+jlink(指向依附于jvex的下一条边)+info(权)
例:
图的遍历
从已给的连通图某个顶点出发,沿着一些边访问图的所有顶点,且每个顶点仅被访问一次
实质:找每个顶点邻接点过程
避免重复访问:设置辅助数组visited[i]用来标记每个被访问的顶点
深度优先搜索DFS
方法:
-
在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w1
-
再从w1出发,访问与w1邻接但还未被访问过的顶点w2
-
然后再从w2出发,进行类似的访问,直至到达所有的邻接顶点都被访问过的顶点u为止
-
退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点
- 有:访问此顶点,之后再从此顶点出发,进行与前述类似的访问
- 无:再退回一步进行搜索,重复上述过程,直到连通图中所有顶点都被访问过为止
#define MVNum 100 //表示最大顶点数
typedef struct {
char vexs[MVNum];
int arcs[MVNum][MVNum];
int vexnum,arcnum;
}AMGraph;
void DFS(AMGraph G,int v){
printf("Please input the v:\n");
scanf("%d",&v);
int visited[v];
for (int i = 0; i <G.vexnum ; ++i) {
visited[i]=0;
}
for (int j = 0; j <G.vexnum ; ++j) {
if((G.arcs[v][j]!=0)&&!visited[j]) DFS(G,j);
}
}
邻接矩阵时间复杂度O(n^2);邻接表时间复杂度O(n+e)
广度优先搜索BFS
方法:从图的某一结点出发,首先访问该结点的所有邻接点vi1,vi2,...,vin再按这些顶点被访问的先后次序依次访问与它们相邻且未被访问的结点,直至所有结点均被访问为止
#define MVNum 100 //表示最大顶点数
typedef struct {
char vexs[MVNum];
int arcs[MVNum][MVNum];
int vexnum,arcnum;
}Graph;
typedef struct {
int data[MVNum];
int front,rear;
}SqQueue;
void InitQueue(SqQueue *Q){
Q->front=Q->rear=0;
}
void EnQueue(SqQueue *Q,int v){
if((Q->rear+1)%MVNum==Q->front) printf("It is full.");
Q->data[Q->rear]=v;
Q->rear=(Q->rear+1)%MVNum;
}
void DeQueue(SqQueue *Q,int *v){
if(Q->rear==Q->front) printf("It is empty.");
*v=Q->data[Q->front];
Q->front=(Q->front+1)%MVNum;
}
void BFS(Graph G){
SqQueue Q;
int visited[G.vexnum];
for (int i = 0; i <G.vexnum ; ++i) {
visited[i]=0;
}
InitQueue(&Q);
for (int j = 0; j <G.vexnum ; ++j) {
if(!visited[j]){
EnQueue(&Q,j);
visited[j]=1;
while (Q.front!=Q.rear){ //队不为空出队
DeQueue(&Q,&j);
for (int i = 0; i <G.vexnum ; ++i) { //找邻接点
if(G.arcs[j][i]==1&&!visited[i]){
visited[i]=1;
EnQueue(&Q,i);
}
}
}
}
}
}
邻接矩阵时间复杂度O(n^2);邻接表时间复杂度O(n+e)
DFS与BFS比较
-
空间复杂度相同 ,都为哦O(n) (借用了栈或队列)
-
时间复杂度只与存储结构有关,与搜索路径无关
图的应用
最小生成树MST
无向图的生成树:将图遍历一遍所经过的边+顶点
最小生成树/最小代价生成树:给定一个无向网,该网所有生成树中,各边权值之和最小的生成树
应用:建立通信网
MST 性质(贪心算法):设N =(V,E)是一个连通网,U是顶点集V的一个非空子集,若边(u, v)是一条具有最小权值的边,其中u∈U, v∈V-U,则必存在一棵包含边(u, v)的最小生成树
-
U:已落在生成树上的顶点集
-
V-U:尚未落在生成树上的顶点集
在所有连通U,和V-U顶点边中权值最小的
构造最小生成树
可能不唯一
-
Prim算法
-
设N=(V,E)是连通网,TE是N上最小生成树中边的集合
-
初始令U={uo}, (u0∈ V),TE={}
-
在所有u∈U, v∈ V-U的边(u,v)∈E中,找一条代价最小的边(u0,v0)
-
将(U0,v0)并入集合TE,同时v0并入U
-
重复上述操作直至U=V为止,则T=(V,TE)为N的最小生成树
-
-
Kruskal算法
-
设连通网N= (V,E),令最小生成树初始状态为只有n个顶点而无边的非连通图T=(V,{ }),每个顶点自成一个连通分量
-
在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边
-
依此类推,直至T中所有顶点都在同一连通分量上为止(有n-1条边)
-
两种算法比较:
算法名 | Prim | Kruskal |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2) | O(eloge) |
适应范围 | 稠密图 | 稀疏图 |
最短路径
在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径
单源最短路径
Dijkstra算法 时间复杂度O(n^2)
步骤:
-
初始时令S={ vo},T={其余顶点};T中顶点对应的距离值用辅助数组D存放
-
D[i]初值:若<v0,Vi>存在,则为其权值;否则为∞
-
从T中选取一个其距离值最小的顶点vj,加入S
-
加进Vj作中间顶点,若v0到vi的距离值变短,则修改此距离值
-
重复上述步骤,直到S=V
所有顶点间的最短路径
-
执行n次Dijkstra算法(时间复杂度是O(n^3))
-
Floyd算法 O(n^3)
步骤;
-
设置一个n阶方阵:对角线元素都是0,<vi,vj>为对应权值,否则为∞
-
试探加入中间结点,是否有更短路径,有则修改
-
直至所有顶点都加入
-
拓扑排序
-
对于有向无环图DAG图
-
常来描述工程或系统的进行过程
-
将AOV网排列成一个线性序列且包含了网中的前后关系
AOV网/顶点表示活动的网
用一个有向图表示一个工程的各个子工程及其相互制约关系,其中顶点表示活动,弧表示活动之间的优先制约关系
特点:
-
若从i到j/<i,j>有一条有向路径,则i是j的前驱/直接前驱,j是i的后继/直接后继
-
不允许有回路:表名某项活动以自己为先决条件
方法:
-
在有向图中选择一个没有前驱的顶点且输出
-
从图中删除该顶点和以该顶点为弧尾的边
-
重复上面两步,直至全部顶点均输出或当图中不存在无前驱的顶点
一个AOV网的拓扑序列不唯一
若某些顶点不在拓扑序列中,则该AOV网存在环
关键路径
AOE网/边表示活动的网
用一个有向图表示一个工程的各个子工程及其相互制约关系,其中弧表示活动,顶点表示活动的开始或结束事件,弧的权表示活动持续时间
源点:入度为0的点
汇点:出度为0的点
关键路径:从源点到汇点路径长度最长(活动持续时间之和最长)的路径
-
ve(vj):事件vj的最早发生时间
-
vl(vj):事件vj的最晚发生时间
-
e(i):活动ai最早开始时间
-
l(i):活动ai最晚开始时间
l(i)-e(i):完成活动ai的时间余量
关键活动:关键路径上的活动即l(i)==e(i)
e(i)=ve(vj);l(i)=vl(vj)- 持续时间
ve找入度边权值最大;vl找出度边权值最小
e找ai活动的源点所对应ve;l找ai 活动终点所对应vl-活动持续时间
l-e==0的活动即为关键活动;关键活动构成的路径即为关键路径
-
若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动
-
如果一个活动处于所有关键路径上,提高这个活动速度可以加快整个工程时间,但不能缩短太多,否则会使原来的关键路径不再是关键路径,这时必须重新寻找关键路径