【高阶数据结构】第二弹---图的深度解析:从基本概念到邻接矩阵的存储与操作

【高阶数据结构】第二弹---图的深度解析:从基本概念到邻接矩阵的存储与操作

✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【高阶数据结构】

目录

1、图的基本概念

2、图的存储结构

2.1、邻接矩阵

2.1.1、基本结构

2.1.2、图的创建

2.1.3、获取顶点下标

2.1.4、添加边

2.1.5、打印

2.1.6、测试


1、图的基本概念

图(Graph)是由顶点集合(V)及顶点间的边的集合(E)组成的一种数据结构G = (V, E),其中:

  • G表示图
  • 顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;
  • 顶点之间的边集合E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。

顶点和边图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
 

有向图和无向图在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意无向边(x, y)等于有向边<x, y>和<y, x>
完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在有n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。



邻接顶点:在无向图G中若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联



顶点的度顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。
 

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径



路径长度:对于不带权的图一条路径的路径长度是指该路径上的边的条数;对于带权的图一条路径的路径长度是指该路径上各个边权值的总和。 
简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环
子图设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图
连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图



强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图



生成树:一个连通图的最小连通子图称作该图的生成树有n个顶点的连通图的生成树有n个顶点和n-1条边

2、图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

2.1、邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。 

注意:

  • 1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度
  • 2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替。 
  • 3. 用邻接矩阵存储图的优点能够快速知道两个顶点是否连通缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求。 

2.1.1、基本结构

1、为了图的灵活性,此处使用模板来描述图,使用类型模板参数描述顶点和边的类型,使用非类型模板参数描述无穷大和图的方向



2、使用vector存储顶点集合map存储顶点与下标的映射关系二维vector存储边集合的矩阵
// vertex: 顶点 edge: 边 weight: 权值 template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { typedef Graph<V, W, MAX_W, Direction> Self; public: // ... private: vector<V> _vertexs; // 顶点集合 map<V, size_t> _vIndexMap; // 顶点映射下标 vector<vector<W>> _matrix; // 存储边集合的矩阵 };

2.1.2、图的创建

图有三种常见的创建方式,如下:

1、IO输入 -- 不方便测试,OJ中更适合

2、图结构关系写到文件,读取文件

3、手动添加边(此处使用手动添加边)
构造函数传入顶点集合以及顶点个数为了能够生成默认构造函数,此处需要使用default关键字。 
// 生成默认构造 Graph() = default; Graph(const V* vertexs, size_t n) { _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(vertexs[i]); _vIndexMap[vertexs[i]] = i; } // MAX_W 作为不存在边的标识值 _matrix.resize(n); for (auto& e : _matrix) { e.resize(n, MAX_W); } }

2.1.3、获取顶点下标

顶点与下标的映射存储在map中,获取顶点对应的下标只需要在map中进行查找即可,找到则返回下标,没找到则抛异常(为了编译通过,返回-1)。
size_t GetVertexIndex(const V& v) { auto ret = _vIndexMap.find(v); if (ret != _vIndexMap.end()) { return ret->second; } else { //assert(false); throw invalid_argument("顶点不存在"); return -1; // 编译通过 } }

2.1.4、添加边

1、此处使用的是手动添加边的方式创建图,因此添加边的函数是必不可少的。参数为起始顶点,结束顶点和权值,还需注意有向与无向图

2、添加边的思想先获取顶点对应的下标,然后将权值存进矩阵中,如果是无向图则需双向存储

参数为顶点值

// 添加边(直接实现) void AddEdge(const V& src, const V& dst, const W& w) { // 查找边的下标 size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); _matrix[srci][dsti] = w; // 无向图 if (Direction == false) { _matrix[dsti][srci] = w; } }

参数为顶点下标

void _AddEdge(size_t srci, size_t dsti, const W& w) { _matrix[srci][dsti] = w; // 无向图 if (Direction == false) { _matrix[dsti][srci] = w; } }

复用子函数方式

// 添加边(调用子函数) void AddEdge(const V& src, const V& dst, const W& w) { // 查找边的下标 size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); _AddEdge(srci, dsti, w); }

2.1.5、打印

打印的方式可以根据自己需求打印,此处打印下标与顶点的映射关系和矩阵,为了让打印更美观,将矩阵的对角线打印成0,无穷大值打印成*,并打印矩阵的行列
void Print() { // 打印下标和顶点映射关系 for (size_t i = 0; i < _vertexs.size(); i++) { cout << "[" << i << "]->" << _vertexs[i] << endl; } cout << endl; // 横下标 cout << " "; for (size_t i = 0; i < _vertexs.size(); i++) { //cout << i << " "; printf("%4d", i); } cout << endl; // 打印矩阵 for (size_t i = 0; i < _matrix.size(); i++) { // 竖下标 cout << i << " "; for (size_t j = 0; j < _matrix[i].size(); j++) { if (i == j) { printf("%4d", 0); } else if (_matrix[i][j] != MAX_W) { //cout << _matrix[i][j] << " "; printf("%4d", _matrix[i][j]); } else { //cout << "* "; printf("%4c", '*'); } } cout << endl; } cout << endl; }

2.1.6、测试

void TestGraph1() { Graph<char, int, INT_MAX, true> g("0123", 4); g.AddEdge('0', '1', 1); g.AddEdge('0', '3', 4); g.AddEdge('1', '3', 2); g.AddEdge('1', '2', 9); g.AddEdge('2', '3', 8); g.AddEdge('2', '1', 5); g.AddEdge('2', '0', 3); g.AddEdge('3', '2', 6); g.Print(); }

构造函数

添加边

打印结果

Read more

傅里叶级数 傅里叶变换 离散时间傅里叶变换(DTFT) 离散傅里叶级数(DFS) 离散傅里叶变换(DFT)快速傅里叶变换(FFT)

傅里叶变换 傅里叶级数 FS 傅里叶变换 FT 时域采样 离散时间傅里叶变换 DTFT 时域采样 离散傅里叶级数 DFS 取有限长视为周期序列的主值周期 取其一个周期 离散傅里叶变换 DFT 频域采样 周期连续信号 离散非周期频谱 非周期连续信号 连续非周期频谱 非周期离散序列 连续周期频谱 周期离散序列 离散周期频谱

By Ne0inhk
初识数据结构——二叉树从基础概念到实践应用

初识数据结构——二叉树从基础概念到实践应用

数据结构专栏 ⬅(click) 初识二叉树:从基础概念到实践应用🌳 一、树型结构基础 1.1 树的基本概念 树是一种非线性的数据结构,由n(n>0)个有限节点组成一个具有层次关系的集合。它看起来像一棵倒挂的树,根朝上而叶朝下。 关键特点:有且仅有一个根节点,没有前驱节点除根节点外,其余节点被分成M(M>0)个互不相交的子树树是递归定义的 重要术语:结点的度:一个结点含有子树的个数树的度:树中所有结点度的最大值叶子结点:度为0的结点双亲结点/父结点:含有子结点的结点孩子结点/子结点:一个结点含有的子树的根结点根结点:没有双亲结点的结点结点的层次:从根开始定义,根为第1层树的高度/深度:树中结点的最大层次 1.2 树的表示方法 最常用的表示方法是孩子兄弟表示法: classNode{int value;// 树中存储的数据Node firstChild;// 第一个孩子引用Node nextBrother;

By Ne0inhk
【狂热算法篇】堆核驱动 TopK 分拣,快选奇招直击数据核心

【狂热算法篇】堆核驱动 TopK 分拣,快选奇招直击数据核心

在数据的浩瀚海洋里,我们常常会遇到这样一类需求:从大量数据中找出最大或最小的前 K 个元素,这就是 TopK 问题。比如在搜索引擎中,要从海量网页里筛选出与用户查询最相关的前 K 个结果;在电商平台,需统计出热销商品的前 K 名。解决 TopK 问题有多种方法,这里着重介绍快速选择法与堆法 。  欢迎拜访:羑悻的小杀马特.-ZEEKLOG博客 本篇主题:深度剖析TOP_K问题解答的快速选择法与堆法 制作日期:2025.05.21 隶属专栏:美妙的算法世界 目录 一.快速选择法: 1.1快选介绍: 1.2时间与空间复杂度分析: 1.2.1时间复杂度: 1.2.2空间复杂度:  1.3 代码实现快选法: top第k 大:

By Ne0inhk
基于YOLOv10算法的交通信号灯检测与识别

基于YOLOv10算法的交通信号灯检测与识别

目录 * 一.🦁 写在前面 * 1.1 实现模块划分 * 1.2 优化与实时性支持 * 二.🦁 相关技术与理论基础 * 2.1 各版本yolo对比 * 2.2 YOLOv10网络结构 * 三.🦁 结果分析 * 3.1 训练损失与验证损失分析 * 3.2 精确率(Precision)、召回率(Recall)和平均精度(mAP)分析 * 四.🦁 实现界面 * 4.1 用户界面实现 * 4.2 检测能力实现 * 4.3 视频处理检测 * 4.4 图片处理检测 * 五.🦁 写在最后 随着城市化进程的加速,交通问题日益严峻,特别是在智能交通和自动驾驶领域,

By Ne0inhk