王道数据结构笔记06—图(C语言)

1.图的基本概念

图的定义

图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。若V={V1,V2…Vn},则用|V|表示图G中的顶点的个数,也称图G的阶,E={(u,v)|u∈V,v∈V},用|E|表示图G中边的条数,图的顶点集一定是非空集,但边可以是空集。

无向边:用()表示
有向边:用 <>表示
简单图:不存在重复边,不存在顶点到自身的边
多重图:某两个结点之间的边数多于一条,或有结点有自回环

顶点的度
无向图:
顶点的度指:依附于该顶点的边的条数
度数之和:边的两倍
有向图:
顶点的入度:以该顶点为终点的有向边的条数
顶点的出度:以该顶点为起点的有向边的条数
顶点的度:入度+出度

路径
两个顶点之间的一条路径指顶点序列
路径长度
路径上边的数目
点到点的距离
一个顶点到另一个顶点的最短路径的长度,若不存在路径,则为∞(无穷)
简单路径
顶点不重复出现的路径
简单回路
除最后和第一个顶点外,其余顶点不重复出现的路径
回路
第一个顶点和最后一个顶点相同的路径
连通:
无向图中,两个顶点间有路径存在,称这两个顶点连通
强连通:
有向图中,两个顶点从其中一个到另一个均有路径,则这两个顶点是强连通的
连通图
图G中任意两个顶点都是连通的
对于n个结点的无向图G,若是连通图,则最少有n-1条边,若是非连通图,则最多可能偶C2n-1 条边
强连通图
图中任意一对结点都是强连通的
对于n个结点的有向图G,若是强连通图,则最少有n条边
子图
一个图的子集和边集均是另一个图的子集
生成子图
包含原图所有顶点的子图
连通分量
无向图中的极大连通子图
在这里插入图片描述
强连通分量
有向图中的极大强连通分量
在这里插入图片描述
生成树
连通图的生成树是包含图中全部顶点的一个极小连通子图(n个顶点,n-1条边)
在这里插入图片描述
生成森林
在非连通图中,连通分量的生成树构成了非连通图的生成森林
边的权
在一个图中,每条边都可以标上具有某种意义的数值,该数值称为该边的权值。边上带有权值的图称为带权图,也称网
带权路径长度
当图是带权图时,一条路径上所有边的权值之和。
无向完全图
无向图中任意两个顶点之间都存在边
有向完全图
有向图中任意两个订单之间都存在方向相反的两条弧

2.图的存储

2.1邻接矩阵法

无向图:两个点之间有边为1,无边为0
有向图:有从A到B的边为1,否则为0
在这里插入图片描述
代码定义

#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
    char Vex[MaxVertexNum];//顶点表
    int Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
    int vexnum;//图的当前顶点数
    int arcnum;//图的当前边数
}MGraph;

求结点的度 O(n)
无向图:第i和结点的度:第i行(或第i列)的非零元素个数
第i个结点的出度:第i行非零元素的个数
第i个结点的入度:第i列非零元素的个数

储存带权图
在这里插入图片描述
代码定义

#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 最大的int//宏定义常量无穷
//注:有时候会把带权图中自己指向自己的回路的权值设为0
typedef struct{
    char Vex[MaxVertexNum];//顶点
    int Edge[MaxVertexNum][MaxVertexNum];//边的权
    int vexnum;//顶点数
    int arcnum;//边数
}MGraph1;

性能分析
空间复杂度:O(n2)只和顶点数有关
注:无向图的邻接矩阵是对称矩阵,可以压缩存储。详见:
栈与队列笔记

邻接矩阵法的性质
设图G的邻接矩阵为A,则其中元素An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
邻接矩阵的缺点

空间复杂度较高,只适合存储稠密图,不适合存储稀疏图

2.2邻接表(顺序+链式存储)

在这里插入图片描述
无向图中边结点的数量是2|E|,空间复杂度:O(|V|+2|E|)
结点的度:统计和一个结点有关的边界点数即可
在这里插入图片描述
有向图中边结点的数量是|E|,空间复杂度:O(|V|+|E|)
出度:统计和一个结点有关的边界点数即可
入度与度:遍历,较复杂

#define MaxVertexNum 100 //顶点数目的最大值
//边
typedef struct ArcNode{
    int adjvex;//边指向哪个结点
    struct ArcNode *next;//指向下一条边的指针
    
    //int info; 边权值(非必要)
}ArcNode;

//顶点
typedef struct VNode{
    char data;//顶点信息
    ArcNode *first;//第一条边
}VNode,AdjList[MaxVertexNum];

//用邻接表存储图
typedef struct{
    AdjList vertices;
    int vexnum;
    int arcnum;
}ALGraph;

邻接表的缺点

计算有向图的度与入度只能遍历,较为复杂。

2.3十字链表(有向图)

在这里插入图片描述
空间复杂度:O(|V|+|E|)
出度:顺着绿色的线路找
入度:顺着橙色的线路找

2.4邻接多重表(无向图)

在这里插入图片描述
空间复杂度:O(|V|+|E|)
删除边,删除结点等操作较方便

3.图的基本操作

考研中最常考的是邻接表和邻接矩阵,所以探讨的基本操作都是基于邻接表和邻接矩阵的。

判断边存在
Adjacent(G,x,y):判断图G是否存在边<x,y>或(x,y)

有向图与无向图一样:
邻接矩阵:矩阵中(x,y)的点值是否为1
邻接表:遍历一个x结点对应的边

在这里插入图片描述
列出与结点相邻的边
Neighbors(G.x):列出图G中与结点x邻接的边

邻接矩阵:遍历x对应的列或行
邻接表:遍历和x对应的边界点

在这里插入图片描述
在这里插入图片描述
插入顶点
InsertVertex(G,x):在图G中插入顶点x

只需插入新结点并写入结点信息即可

在这里插入图片描述

删除顶点
DeleteVertex(G,x):从图G中删除顶点x

邻接矩阵:为了避免移动大量元素,我们把要删除顶点的行列值全部修改为0,并且可以在顶点的结构体中增加一个bool型的变量来判断顶点是否存在。

邻接表:若x没有相关联的边,则删除x很容易,若x关联了尽可能多的边,并且在其他结点中,该边的位置又在最后,就会涉及到所有边的遍历。

在这里插入图片描述
在这里插入图片描述
增加一条边
AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则添加。

邻接矩阵:把0修改为1就行
邻接表:头插法或尾插法

在这里插入图片描述

找到第一个邻结点
FirstNeighbor(G,x):求x的第一个邻结点,若有则返回顶点号,若无货x不存在则返回-1

邻接矩阵:遍历即可
邻接表:找到第一条边结点即可

在这里插入图片描述
在这里插入图片描述
找到下一个邻结点
NextNeighbor(G,x,y):设y是顶点x的一个邻结点,返回除y之外顶点x的下一个邻结点的顶点号,若y是x的最后一个邻结点,返回-1

这个操作只需要在上面一个操作的基础上再往后遍历即可

4.广度优先遍历(BFS)

BFS要点:
1.找到与一个顶点相邻的所有顶点(运用上文的FirstNeighbor(G,x),NextNeighbor(G,x))
2.标记哪些顶点被访问过(防止重复访问)
3.需要一个辅助队列

代码实现:
伪代码,主要是代码格式

#define MAX_VERTEX_NUM 100//最大结点数
bool visited[MAX_VERTEX_NUM];//访问标记数组
//广度优先遍历
void BFS(Graph G,int v){//从顶点v出发,广度优先遍历图G
    visit(v);//访问初始顶点v
    visited[v]=true;//对v做已访问标志
    Enqueue(Q,v);//顶点v入队列Q
    while(!isEmpty(Q)){
        DeQueue(Q,v);//顶点v出队列
        for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
            if(!visited[w]){
                visit(w);//访问顶点w
                visited[w]=true;
                EnQueue(Q,w);//顶点w入队列
            }
        }
    }
}

时间复杂度邻接矩阵:O(|V|²)
时间复杂度邻接表:O(|E|+|V|)
在这里插入图片描述
BFS的调用

void    BFSTraverse(Graph G){
   for(int i=1;i<=G.vexnum;i++){
       visited[i]=false;//初始化标记数组
   }
   
   InitQueue(Q);//初始化辅助队列Q
   for(int i=1;i<=G.vexnum;i++){
       if(!visited[i])
           BFS(G,i);
   }
}

广度优先生成树
在这里插入图片描述

5.深度优先遍历(DFS)

#define MAX_VERTEX_NUM 100//最大结点数
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
    for(v=1;v<=G.versum;v++){
        visited[v]=false;//初始化访问标记数组
    }

    for(v=1;v<=G.versum;v++){
        if(!visited[v])
            DFS(G,v);
    }
}

void DFS(Graph G,int v){
    visit(v);//访问顶点v
    visited[v]=true;//标记已访问
    for(w=FirstNeighbor(G,v);w>=1;w=NextNeighbor(G,v,w))//w为u的尚未访问的邻接结点
         if(!visited[w]){
            DFS(G,w);
         }
}

复杂度分析:
空间复杂度:取决于函数调用栈的深度
在这里插入图片描述
时间复杂度:同BFS

深度优先遍历序列的例子:
在这里插入图片描述

6.图的应用

6.1最小生成树(最小代价树)

对于带权的连通无向图G=(V,E),生成树不同,每颗树的权也可能不同,设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树。

6.1.1Prim算法

从某一个顶点开始构建生成树,每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度:O(|V|²),适用于边稠密图

在这里插入图片描述
P城->学校->矿场->渔村->农场->电站
注:同一个图可能有多个最小生成树,其全值之和都相同

6.1.2Kruskal算法

每次选择一条权值最小的边,使这条边的两头相通(原本已连通的就不选),直到所有顶点都连通
时间复杂度:O(|E|log2|E|),适用于边稀疏图

在这里插入图片描述
学校P城->渔村矿场->农场电站->矿场P城->农场P城
在这里插入图片描述

6.2最短路径

6.2.1单源最短路径(BFS)

图中的一个顶点到其余顶点的最短路径

伪代码

//求顶点u到其他顶点的最短路径
void BFS_Distance(Graph G,int u){
    //d[i]表示从u到i结点的最短路径
    for(int i=0;i<G.vexnum;i++){
        d[i]=;//初始化路径长度
        path[i]=-1;//最短路径从哪个顶点过来
    }
    
    d[u]=0;
    visited[u]=true;
    EnQueue(Q,u);
    while(!isEmpty(Q)){
        DeQueue(Q,u);//队头元素u出队
        for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u)){
            if(!visited[i]){//w为u的尚未访问的邻接结点
                d[w]=d[u]+1;//路径长度加1
                path[w]=u;//记录前驱
                visited[w]=true;//标记已访问
                EnQueue(Q,w);//顶点w入队
            }
        }
    }
}

在这里插入图片描述

6.2.2最短路径(Dijkstra算法)

6.2.1中的BFS只适用于无权图,接下来针对有权图,提出迪杰斯特拉算法

在这里插入图片描述
步骤:

1.循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点vi,令final[i]=true.
2.检查所有邻接自Vi的顶点,若其final值为false,则更新dist和path

第一轮后:
在这里插入图片描述
第二轮后:
在这里插入图片描述
第三轮后:
在这里插入图片描述
在这里插入图片描述

算法实现的大致思路:
考研初试主要是手算
在这里插入图片描述

6.2.3最短路径问题(Floyd算法)

求出每一对顶点之间的最短路径(动态规划)

算法实现思路:
在这里插入图片描述
初始情况:不允许在其他顶点中转的最短路径

在这里插入图片描述
若允许在V0中转,依次遍历修改表:
在这里插入图片描述
若允许在V0、V1中转,依次遍历修改表:
在这里插入图片描述
若允许在V0、V1、V2中转,依次遍历修改表:
在这里插入图片描述
在这里插入图片描述
核心代码

//...准备工作,初始化A和path
for(int k=0;k<n;k++){//考虑以Vk作为中转结点
    for(int i=0;i<n;i++){//遍历整个矩阵,i为行号,j为列号
        for(int j=0;j<n;j++){
            if(A[i][j]>A[i][k]+A[k][j]){//若以Vk为中转点的路径更短
                A[i][j]=A[i][k]+A[k][j];
                path[i][j]=k;
            }
        }
    }
}

时间复杂度:O(|v|³)
空间复杂度:O(|v|³)

该算法以动态规划的思想,包括了需要一个或者需要一个及以上中转点的情况,因为每一次修改都是基于在上一个中转点之上进行的。

6.2.4三种算法的对比

在这里插入图片描述

6.3有向无环图描述表达式

若一个有向图中不存在环,则称为有向五环图,简称(DAG图)

这一类题型并不常考,只需了解

1.把各个操作数不重复地排成一排
2.标出各个运算符的生效顺序
3.按照顺序分层加入运算符
4.从底向上检查同层的运算符是否可以合体。

6.4拓扑排序

用DAG图表示一个工程,顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行

拓扑排序的实现:找到做事的先后顺序
1.从图中选择一个没有前驱(入度为0)的顶点并输出
2.从图中删除该顶点和所有以它为起点的有向边
3.重复1和2直到图为空或图中不存在无前驱的结点为止
在这里插入图片描述
在这里插入图片描述

6.5关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成活动的开销,简称AOE网,其具有以下性质:
1.只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
2.只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的时间才能发生,另外,有些活动是可以并行进行的。

在这里插入图片描述
1.在AOE网中仅有一个入度为0的顶点,成为开始顶点,表示工程的开始
2.也只有一个出度为0的点,成为结束顶点,表示工程的结束。

关键路径:从开始到结束顶点所有路径中,具有最大路径长度的路径称为关键路径,其上的活动称为关键活动

完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长

上图中的关键活动:洗番茄,切番茄,炒菜。

事件的最早发生时间:决定了所有从该事件开始的活动能够开工的最早时间
事件的最迟发生时间:在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
活动的最早开始时间:该活动弧的起点表示的事件的最早发生时间
活动的最迟开始时间:该活动弧的终点表示的时间的最迟发生时间与该活动所需时间之差
活动的时间余量:最晚开始时间-最早开始时间,余量为0的即为关键活动

求关键路径:
在这里插入图片描述

一、求所有事件的最早发生时间
在这里插入图片描述
二、求所有时间的最迟发生事件
在这里插入图片描述
三、求各个活动的最早发生时间
四、求各个活动的最迟发生时间
五、求所有活动的时间余量

  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值