• 数据结构详细笔记——图



    图的定义

    图G顶点集V边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合,用|V|表示图G中顶点的个数,也称图G的阶,用|E|表示图G中边的条数

    注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集

    有向图
    若E是有向边(也称)的有限集合时,则图G为有向图。弧是顶点的有序对,记为,其中v、w是顶点,v称为弧尾,w称为弧头。 不等于

    无向图
    若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的有序对,记为(v,w)(v,w) 等于 (w,v)

    简单图
    ①不存在重复的边
    ②不存在顶点到自身的边

    多重图
    图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联

    顶点的度、入读、出度
    对于无向图顶点v的度是指依附于该顶点的边的条数,记为TD(v)
    对于有向图
    入度是以顶点v为终点的有向边的数目,记为ID(v)
    出度是以顶点v为起点的有向边的数目,记为OD(v)
    顶点v的度等于其入度和出度之和,即TD(v)=ID(v)+OD(v)

    顶点-顶点的关系

    - 路径——顶点到顶点之间的一条路径

    • 回路——第一个顶点和最后一个顶点相同的路径称为回路或环
    • 简单路径——在路径序列中,顶点不重复出现的路径称为简单路径
    • 路径长度——路径上,边的数目
    • 点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离;若从u到v根本不存在路径,则该距离为无穷(∞)
    • 无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的
    • 有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的

    连通图、强连通图
    若图G中 任意两个顶点都是连通的,则称图G为 连通图,否则称为非连通图

    对于n个顶点的无向图G
    若G是连通图,则最少有 n-1 条边
    若G是非连通图,则最多可能有在这里插入图片描述条边

    若图中 任何一对顶点都是强连通的,则称此图为 强连通图

    对于n个顶点的有向图G
    若G是强连通图,则最少有n条边(形成回路)

    图的局部——子图
    设有两个图G(V,E)和G’=(V’,E’),若V’是V的子集,且E’是E的子集,则称G‘是G的 子图

    若有满足V(G’)=V(G)—— 顶点相同的子图G’,则称其为G的 生成子图

    连通分量
    无向图中的 极大连通子图(子图必须连通,且包含尽可能多的顶点和边)称为连通分量

    强连通分量
    有向图中的 极大强连通子图(子图必须连通,且包含尽可能多的顶点和边)称为强连通分量

    生成树
    连通图的生成树包含图中全部顶点的一个极小连通子图

    若图中顶点数为n,则它的生成树含有n-1条边,对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路

    生成森林
    在非连通图中,连通分量的生成树构成了非连通图的生成森林

    边的权、带权图/网
    边的权——在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值
    带权图/网——边上带有权值的图称为带权图,也称网
    带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度

    几种特殊形态的图
    无向完全图——无向图中任意两个顶点之间存在边
    有向完全图——有向图中任意两个顶点之间都存在方向相反的两条弧
    树——不存在回路,且连通的无向图
    有向树——一个顶点的入度为0,其余顶点的入度均为1的有向图

    总结

    对于n个顶点的无向图G
    所有顶点的度之和等于2|E|
    若G是连通图,则最少有n-1条边(树),若 |E|>n-1,则一定有回路
    若G是非连通图,则最多可能有在这里插入图片描述条边
    无向完全图共有在这里插入图片描述条边

    对于n个顶点的有向图G
    所有顶点的出度之和=入度之和= |E|
    所有顶点的度之和等于 2|E|
    若G是强连接图,则最少有n条边(形成回路)
    有向完全图共有在这里插入图片描述条边

    图的存储

    邻接矩阵法

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

    邻接矩阵存储带权图

    #define MaxVertexNum 100              // 顶点数量的最大值
    #define INFINITY                      // 宏定义常量“无穷”
    typedef char VertexType;              // 顶点的数据类型
    typedef int EdgeType;                 // 带权图中边上权值的数据类型
    typedef struct{
    	VertexType Vex[MaxVertexNum];     // 顶点
    	EdgeType Edge[MaxVertexNum][MaxVertexNum];    // 边的权
    	int vexnum,arcnum;                // 图的当前顶点数和弧数
    }MGraph;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    空间复杂度
    O(|V|²) ——只和顶点数相关,和实际的边数无关
    适合于存储稠密图
    无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)

    邻接矩阵法的性质
    设图G的邻接矩阵为A(矩阵元素为0/1),则Aⁿ的元素Aⁿ[ i ][ j ]等于由顶点 i 到顶点 j 的长度为 n 的路径的数目

    举个🌰:
    A:
    | A | B | C | D |
    | A | 0 | 1 | 0 | 0 |
    | B | 1 | 0 | 1 | 1 |
    | C | 0 | 1 | 0 | 1 |
    | D | 0 | 1 | 1 | 0 |

    A² [ 1 ] [ 4 ] = a(1,1)a(1,4) + a(1,2)a(2,4) + a(1,3)a(3,4) + a(1,4)a(4,4) = 1
    说明 顶点A到顶点D的长度为2的路径有1条

    邻接表法

    邻接表法——顺序+链式存储

    // 边/弧
    typedef struct ArcNode{
    	int adjvex;       // 边/弧指向哪个结点
    	struct ArcNode *next;  // 指向下一条弧的指针
    }ArcNode;
    
    // 顶点
    typedef struct VNode{
    	VertexType data;    // 顶点信息
    	ArcNode *first;     // 第一条边/弧
    }VNode,AdjList[MaxVertexNum];
    
    // 用邻接表存储的图
    typedef struct{
    	AdjList vertices;
    	int vexnum,arcnum;
    }ALGraph;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    无向图——边结点的数量是2|E|,整体空间复杂度为 O(|V|+2|E|)
    有向图——边结点的数量是|E|,整体空间复杂度为 O(|V|+|E|)

    邻接矩阵法与邻接表法的区别

    邻接表邻接矩阵
    空间复杂度无向图:O(∣V∣+2∣E∣);有向图:O(∣V∣+∣E∣)O(∣V∣²)
    适用于存储稀疏图存储稠密图
    表示方式不唯一唯一
    计算度/出度/入度计算有向图的度、入度不方便,其余很方便必须遍历对应行或列
    找相邻的边找有向图的入边不方便,其余很方便必须遍历对应行或列

    图的基本操作

    • Adjacent(G,x,y):判断图G是否存在边或(x,y)
      邻接矩阵————O(1)
      邻接表 ————O(1)~O(|V|)

    • Neighbors(G,x):列出图G中与结点x邻接的边
      无向图:
      邻接矩阵————O(|V|)
      邻接表 ————O(1)~O(|V|)
      有向图:
      邻接矩阵————O(|V|)
      邻接表 ————出度:O(1)~O(|V|);入度:O(|E|)

    • InsertVertex(G,x):在图G中插入顶点x
      邻接矩阵————O(1)
      邻接表 ————O(1)

    • DeleteVertex(G,x):在图G中删除顶点x
      无向图:
      邻接矩阵————O(|V|)
      邻接表 ————O(1)~O(|V|)
      有向图:
      邻接矩阵————O(|V|)
      邻接表 ————删出边:O(1)~O(|V|);删入边:O(|E|)

    • AddEdge(G,x,y):若无向边(x,y)或有向边不存在,则向图G中添加该边
      邻接矩阵————O(1)
      邻接表 ————O(1)

    • FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号,若没有邻接点或图中不存在x,则返回-1
      无向图:
      邻接矩阵————O(1)~O(|V|)
      邻接表 ————O(1)
      有向图:
      邻接矩阵————O(1)~O(|V|)
      邻接表 ————找出边:O(1);找入边:O(1)~O(|E|)

    • NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1
      邻接矩阵————O(1)~O(|V|)
      邻接表 ————O(1)

    • Get_edge_value(G,x,y):获取图G中边(x,y)或对应的权值

    • Set_edge_value(G,x,y):设置图G中边(x,y)或对应的权值

    图的遍历

    广度优先遍历(BFS)

    图的广度优先遍历和树的层次遍历很相似
    代码实现:

    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)){
    			// 检测v所有邻接点
    			if(!visited[w]){    // w为v的未访问的邻接顶点
    				visit(w);           // 访问顶点w
    				visited[w]=true;    // 对w做已访问标记
    				EnQueue(Q,w);       // 顶点w入队列
    			}
    		}
    	}
    }
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如果是非连通图,则无法遍历完所有的结点,以上的代码就出现了Bug
    所以先对所有的顶点遍历,还需要加上以下代码

    void BfsTraverse(Graph G){
    	for(i=0;i<G.vexnum;++i){
    		visited[i]=false;  // 访问标记数组
    	}
    	InitQueue(Q);          // 初始化辅助队列Q
    	for(i=0;i<G.vexnum;++i){   // 从0号顶点开始遍历
    		if(!visited[i])    // 对每一个连通分量调用依次BFS
    			BFS(G,i);      // vi未访问过,从vi开始BFS
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结论:对于无向图,调用BFS函数的次数=连通分量数

    注意:
    同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
    同一个图的邻接表表示方式不唯一,因此广度优先遍历序列不唯一

    复杂度分析
    对于邻接矩阵存储的图:O(|v|²)
    对于邻接表存储的图:O(|V|+|E|)

    广度优先生成树
    由广度优先遍历过程确定,将遍历过程中没有经过的边去掉

    由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一

    遍历非连通图可以得到广度优先生成森林

    深度优先遍历(DFS)

    图的深度优先遍历和树的先根遍历很相似

    bool visited[MAX_VERTEX_NUM];   // 访问标记数组
    void DFSTraverse(Graph G){
    	for(v=0;v<G.vexnum;++v)
    		visited[v]=false;
    	for(v=0;v<G.vexnum;++v)
    		if(!visited[v])
    			DFS(G,v)
    }
    
    
    void DFS(Graph G,int v){
    	visit(v);
    	visited[v]=true;
    	for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
    		if(!visited[w]){
    			DFS(G,w);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    结论:对于无向图,调用D FS函数的次数=连通分量数

    复杂度分析
    空间复杂度:O(|v|)
    对于邻接矩阵存储的图:O(|v|²)
    对于邻接表存储的图:O(|V|+|E|)

    深度优先遍历序列

    注意:
    同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一
    同一个图的邻接表表示方式不唯一,因此深度优先遍历序列不唯一

    深度优先生成树
    由深度优先遍历过程确定,将遍历过程中没有经过的边去掉

    由于邻接表的表示方式不唯一,因此基于邻接表的深度优先生成树也不唯一

    遍历非连通图可以得到深度优先生成森林

    图的遍历和图的连通性

    无向图:
    DFS/BFS函数调用次数=连通分量数
    有向图:
    1、若从起始顶点到其他顶点都有路径,则只需要调用1次DFS/BFS函数
    2、对于强连通图,从任一顶点出发都只需要调用1次DFS/BFS函数

  • 相关阅读:
    YOLOv8改进 | 注意力机制 | 在主干网络中添加MHSA模块【原理+附完整代码】
    算法竞赛入门【码蹄集进阶塔335题】(MT2291-2295)
    赤壁
    Algorithm基础算法学习
    【RepVGG网络】
    Debezium系列之:MySQL创建具有binlog权限的用户
    python学习-7-异常 错误 调试 测试
    【项目管理】甘特图(1)——认识甘特图
    STM32的BOOT1和BOOT0查找及配置-都有BOOT1引脚的
    【PCB学习笔记】绘制智能车四层板 ---原理图绘制及编译检查
  • 原文地址:https://blog.csdn.net/weixin_44732078/article/details/134245450