【数据结构】基础:图的基本概念与实现(附C++源代码)

news2024/9/20 21:22:55

【数据结构】基础:图的基本概念与实现(附C++源代码)

摘要:将会在数据结构专题中开展关于图论的内容介绍,其中包括四部分,分别为图的概念与实现、图的遍历、图的最小生成树以及图的最短路径问题。本文将介绍图的基本概念以及相关内容,再对图的常见实现方式进行介绍。实现方法为邻接矩阵法与邻接表法,从其成员实现、构造、边的添加出发,最后对二者进行比较。


文章目录

  • 【数据结构】基础:图的基本概念与实现(附C++源代码)
    • 一、图的基本概念
      • 1.1 图的定义
      • 1.2 图的相关概念
    • 二、图的实现
      • 2.1 邻接矩阵
        • 2.1.1 成员实现
        • 2.1.2 构造函数
        • 2.1.3 边的添加
        • 2.1.4 打印函数
        • 2.1.5 测试用例
      • 2.2 邻接表
        • 2.2.1 成员实现
        • 2.2.2 构造函数
        • 2.2.3 边的添加
        • 2.2.4 打印函数
        • 2.2.5 测试用例
      • 2.3 对比

一、图的基本概念

1.1 图的定义

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

  • 顶点集合V = {x|x∈G中顶点},V(G)表示图G中顶点的有限非空集;
  • 边集合E = {(x,y)|x,y∈V}或者E = {<x, y>|x,y∈V && Path(x, y)},E(G)是顶点间关系的有穷集合,也叫做边的集合。(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。

1.2 图的相关概念

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

  • 有向图和无向图

    • 在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y><y, x>是两条不同的边。

    • 在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)(y,x)是同一条边。注意:无向边(x, y)等于有向边<x, y><y, x>

  • 完全图

    • 无向完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边

    • 有向完全图:在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边

  • 邻接顶点:在无向图中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的路径。

  • 路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和

  • 子图:设图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.1 邻接矩阵

邻接矩阵法是通过一个二维数组及其他附属数据结构完成对于图的存储,具体内容如下:

  • 一维数组:记录顶点及其元素,每个顶点都对应着一个检索,该检索通过一棵树进行储存在这里采用了map
  • 索引树:记录了数组索引和顶点元素之间的关系
  • 二维矩阵:表示顶点之间边的关系,可以记录为是否连通或记录两点之间的权重

2.1.1 成员实现

在此使用C++实现,其中添加了Direction 表示是否为有向图,MAX_WEIGHT表示最大权值

template<class V, class W, bool Direction = false, W MAX_WEIGHT = INT_MAX>
    class Graph {
        private:
        vector<V> _vertexs; // 顶点集合
        map<V, int> _vIndexMap; // 顶点检索
        vector<vector<W>> _matrix; // 邻接矩阵
    };

2.1.2 构造函数

对于图的初始化构建,将容量进行对于n个结点的扩容,将节点元素写入,并将结点元素与对应的索引记录再map中。对于矩阵而言,形成n × n的扩容,赋初值为最大权值,在有些实现方法中,会对图的对角线进行取零也是可以的,在此不进行该操作。

Graph() = default;
/// <summary>
/// _vertexs扩容即可
/// _matrix赋初值Max_Weight 对角线为0
/// </summary>
Graph(const V* vertexs,size_t vertexSize) {
    _vertexs.reserve(vertexSize);
    for (size_t i = 0; i < vertexSize; i++) {
        _vertexs.push_back(vertexs[i]);
        _vIndexMap[vertexs[i]] = i;
    }
    // 格式化
    _matrix.resize(vertexSize);
    for (auto& e : _matrix) {
        e.resize(vertexSize, MAX_WEIGHT);
    }
    // 对角线写0
    //for (size_t i = 0; i < _matrix.size(); i++) {
    //	_matrix[i][i] = 0;
    //}
}

2.1.3 边的添加

实现图边添加的方式主要有三种,分别为基础IO、文件添加以及函数调用。在此使用函数调用的方式,而具体实现方式是在邻接矩阵写入对应权值即可。对于有向与无向而言,无向图需要在对称位置写入对称的权值,具体代码如下:

/// <summary>
/// 在map中检索对应的索引
/// </summary>
/// <param name="v">顶点中的元素</param>
/// <returns>检索值</returns>
size_t GetVertexIndex(const V& v) {
    auto ret = _vIndexMap.find(v);
    if (ret != _vIndexMap.end()) {
        return ret->second;
    }
    else {
        throw invalid_argument("不存在的顶点");
        return -1;
    }
}
/// <summary>
/// 添加边 找出索引调用函数AddEdgeByIndex
/// </summary>
/// <param name="src">起点元素</param>
/// <param name="dest">终点元素</param>
/// <param name="weight">权重</param>
void AddEdge(const V& src, const V& dest, const W& weight) {
    size_t srcIndex = GetVertexIndex(src);
    size_t destIndex = GetVertexIndex(dest);
    AddEdgeByIndex(srcIndex, destIndex, weight);
}
/// <summary>
/// 在矩阵中写入对应权值
/// 无向图需要再写入对称位置
/// </summary>
/// <param name="srcIndex">起点检索</param>
/// <param name="destIndex">终点检索</param>
/// <param name="weight">权重</param>
void AddEdgeByIndex(size_t srcIndex, size_t destIndex, const W& weight){
    _matrix[srcIndex][destIndex] = weight;
    if (Direction == false) {
        _matrix[destIndex][srcIndex] = weight;
    }
}

2.1.4 打印函数

  • 打印顶点和下标映射关系
  • 打印矩阵
  • 打印所有的边
void Print() {
    // 打印顶点和下标映射关系
    cout << "顶点与下标的映射关系:" << endl;
    for (size_t i = 0; i < _vertexs.size(); ++i) {
        cout << "[" << _vertexs[i] << "]" << "->" << "[" << i << "]" << endl;
    }
    cout << endl;

    cout << "邻接矩阵:" << endl;
    cout << " \t";
    for (size_t i = 0; i < _vertexs.size(); ++i) {
        cout << i << "\t";
    }
    cout << endl;
    // 打印矩阵
    for (size_t i = 0; i < _matrix.size(); ++i) {
        cout << i << "\t";
        for (size_t j = 0; j < _matrix[i].size(); ++j) {
            if (_matrix[i][j] == MAX_WEIGHT)
                cout << "∞" << "\t";
            else
                cout << _matrix[i][j] << "\t";
        }
        cout << endl;
    }
    cout << endl;

    // 打印所有的边
    cout << "打印所有的边:" << endl;
    for (size_t i = 0; i < _matrix.size(); ++i) {
        for (size_t j = 0; j < _matrix[i].size(); ++j) {
            if (i < j && _matrix[i][j] != 0 && _matrix[i][j] != MAX_WEIGHT) {
                cout << "[" << _vertexs[i] << "]" << "->" << "[" << _vertexs[j] << "] : " << _matrix[i][j] << endl;
            }
        }
    }
}

2.1.5 测试用例

void TestGraph(){
	Graph<char, int, true> g("abcd", 4);
	g.AddEdge('a', 'b', 1);
	g.AddEdge('a', 'd', 4);
	g.AddEdge('b', 'd', 4);
	g.AddEdge('b', 'c', 9);
	g.AddEdge('c', 'd', 8);
	g.AddEdge('c', 'b', 5);
	g.AddEdge('c', 'a', 3);
	g.AddEdge('d', 'c', 6);

	g.Print();
}

image-20230213212702126

2.2 邻接表

邻接矩阵法是通过一个二维数组及其他附属数据结构完成对于图的存储,具体内容如下:

  • 一维数组:记录顶点及其元素,每个顶点都对应着一个检索,该检索通过一棵树进行储存在这里采用了map
  • 索引树:记录了数组索引和顶点元素之间的关系
  • 邻接表:通过数组进行储存,索引下标对应顶点,每个数组元素为一个链表,记录与该点连接的边

2.2.1 成员实现

在此使用C++实现,其中添加了Direction 表示是否为有向图,由于后续需要对边进行插入,因此需要进行封装。

template<class V,class W,bool Direction = false>
    class Graph {
        typedef Edge<W> Edge;
        private:
        vector<V> _vertexs;
        map<V, int> _indexMap;
        vector<Edge*> _linkTables;
    };
template<class W>
    class Edge {
        public:
        int _destIndex;
        W _weight;
        Edge<W>* _next;

        Edge(int destIndex,const W& weight) 
            :_destIndex(destIndex)
                ,_weight(weight)
                ,_next(nullptr)
            {}
    };

2.2.2 构造函数

对于图的初始化构建,将容量进行对于n个结点的扩容,将节点元素写入,并将结点元素与对应的索引记录再map中。对于邻接表而言,进行扩容并存储空指针,在后续过程中会进行插入。

Graph(const V* arrV, size_t vSize){
    _vertexs.reserve(vSize);
    for (size_t i = 0; i < vSize; i++) {
        _vertexs.push_back(arrV[i]);
        _indexMap[arrV[i]] = i;
    }
    _linkTables.resize(vSize, nullptr);
}

2.2.3 边的添加

获取边的插入,进行检索顶点,在邻接表中找出对应的位置,对应链表来说,为了提高效率进行头插操作。而对于无向图还需要在目标节点的链表上也添加边,具体代码如下:

size_t GetVertexIndex(const V& value){
    auto it = _indexMap.find(value);
    if (it != _indexMap.end()){
        return it->second;
    }
    else{
        throw invalid_argument("顶点不存在");
        return -1;
    }
}
void AddEdge(const V& src, const V& dest, const W& weight) {
    size_t srcIndex = GetVertexIndex(src);
    size_t destIndex = GetVertexIndex(dest);
    // 链表头插
    Edge* edge = new Edge(destIndex, weight);
    edge->_next = _linkTables[srcIndex];
    _linkTables[srcIndex] = edge;
    // 对称插入
    if (Direction == false) {
        Edge* edge = new Edge(srcIndex, weight);
        edge->_next = _linkTables[destIndex];
        _linkTables[destIndex] = edge;
    }
}

2.2.4 打印函数

打印对应的顶点与检索信息,再打印邻接表的内容,代码如下:

void Print(){
    // 顶点
    for (size_t i = 0; i < _vertexs.size(); ++i){
        cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
    }
    cout << endl;

    for (size_t i = 0; i < _linkTables.size(); ++i){
        cout << _vertexs[i] << "[" << i << "]-> ";
        Edge* cur = _linkTables[i];
        while (cur){
            cout << "[" << _vertexs[cur->_destIndex] << "|" << cur->_destIndex << "|" << cur->_weight << "]->";
            cur = cur->_next;
        }
        cout << "nullptr" << endl;
    }
}

2.2.5 测试用例

void TestGraph(){
	linkTable::Graph<char, int, true> g("abcd", 4);
	g.AddEdge('a', 'b', 1);
	g.AddEdge('a', 'd', 4);
	g.AddEdge('b', 'd', 4);
	g.AddEdge('b', 'c', 9);
	g.AddEdge('c', 'd', 8);
	g.AddEdge('c', 'b', 5);
	g.AddEdge('c', 'a', 3);
	g.AddEdge('d', 'c', 6);
	g.Print();
}

image-20230213220700473

2.3 对比

内容比较邻接矩阵邻接表
适用场景稠密图稀疏图
优点通过下标判断两点间的邻接关系并获得权值适用于查找一个顶点连接出去的边
缺点相对而言不适用于查找一个顶点连接所有边不适用于确定两个点是否连接及权值
空间复杂度O(|V|^2)O(|V|+|E|)

补充:

  1. 代码将会放到:C++/C/数据结构代码链接 ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/343698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

遗传算法(Genetic Algorithm,GA)实现数据排序,python

遗传算法(Genetic Algorithm,GA)实现数据排序&#xff0c;python 遗传算法是一种比较广泛、通用的算法体系&#xff0c;为了说明遗传算法的原理和实现&#xff0c;现在用GA解决一个计算机科学最基本、最古老的问题&#xff1a;排序问题。 需要特别说明的是&#xff0c;遗传算…

【GPLT 三阶题目集】L3-016 二叉搜索树的结构

二叉搜索树或者是一棵空树&#xff0c;或者是具有下列性质的二叉树&#xff1a; 若它的左子树不空&#xff0c;则左子树上所有结点的值均小于它的根结点的值&#xff1b;若它的右子树不空&#xff0c;则右子树上所有结点的值均大于它的根结点的值&#xff1b;它的左、右子树也分…

代码随想录LeetCode | 单调栈问题

前沿&#xff1a;撰写博客的目的是为了再刷时回顾和进一步完善&#xff0c;其次才是以教为学&#xff0c;所以如果有些博客写的较简陋&#xff0c;是为了保持进度不得已而为之&#xff0c;还请大家多多见谅。 预&#xff1a;看到题目后的思路和实现的代码。 见&#xff1a;参考…

两种特征提取方法与深度学习方法对比的小型金属物体分类分析研究

本文讨论了用于对包括螺丝、螺母、钥匙和硬币在内的小型金属物体进行分类的两种特征提取方法的效率&#xff1a;定向梯度直方图 (HOG) 和局部二进制模式 (LBP)。首先提取标记图像的所需特征并以特征矩阵的形式保存。使用三种不同的分类方法&#xff08;非参数 K 最近邻算法、支…

云计算|OpenStack|社区版OpenStack(实务操作---cloud-init的使用)

前言&#xff1a; 接上一篇文章&#xff1a;https://zskjohn.blog.csdn.net/article/details/128931042 我们可以从官方获取到现成的镜像&#xff0c;例如&#xff0c;从Ubuntu 18.04 LTS (Bionic Beaver) Daily Build [20230210]官方下载的bionic-server-cloudimg-amd64.img…

春招Leetcode刷题日记-D2-贪心算法-区间问题

D2-贪心算法-区间问题力扣435. 无重叠区间思路代码力扣435. 无重叠区间 题目链接&#xff1a;435. 无重叠区间 思路 1、贪心策略&#xff1a; 1、题目中&#xff0c;给了若干个区间&#xff0c;现在&#xff0c;我想留下尽可能多的&#xff0c;不重叠的区间 2、考虑&#…

Git配置多仓库账户密码

前言说明&#xff1a; 由于我们在工作中可能会遇到公司是用的gitlab仓库&#xff0c;代码下载需要github仓库&#xff0c;自己的项目需要gitee仓库&#xff0c;如何在同一台电电脑配置多个仓库账户密码就尤为重要。 (一) 配置流程 这里是在windows电脑上配置多仓库以github为例…

使用Spring框架的好处是什么

使用Spring框架的好处是什么&#xff1f; 1、轻量&#xff1a;Spring 是轻量的&#xff0c;基本的版本大约2MB。 2、控制反转&#xff1a;Spring通过控制反转实现了松散耦合&#xff0c;对象们给出它们的依赖&#xff0c;而不是创建或查找依赖的对象们。 3、面向切面的编程(AOP…

JCIM|Chemistry42:一个人工智能驱动的分子设计和优化平台

题目&#xff1a;Chemistry42: An AI-Driven Platform for Molecular Design and Optimization 文献来源&#xff1a;https://doi.org/10.1021/acs.jcim.2c01191 代码&#xff1a;https://insilico.com/pipeline (平台网址) 1.背景介绍 Chemistry42是Insilico Medicine提出…

Git_合并多次提交记录

Git_合并多次提交记录 前言&#xff1a;使用git rebase将多条提交记录合并成一条 应用场景&#xff1a;公司代码分支在master上&#xff0c;张三接到一个开发任务&#xff0c;需要在master上开发一个新功能&#xff0c;首先张三不能直接使用master分支进行开发&#xff0c;因为…

论文阅读:GeoAug: Data Augmentation for Few-Shot NeRF with Geometry Constrain

中文标题&#xff1a;使用几何约束增强小样本神经辐射场 提出的问题 NeRF尽管简介有效&#xff0c;但是往往不能收敛到正确的几何结构。这个问题在小样本学习中尤为明显&#xff0c;往往在没有足够训练数据的情况下&#xff0c;很难使得MLP网络学习到正确的几何的隐表示&#…

Solon Java Framework 2.1.2 发布。简单!高效!国产!

Solon 是一个高效的 Java 应用开发框架&#xff1a;更快、更小、更简单。它不是 Spring&#xff0c;没有用 Servlet&#xff0c;也无关 JavaEE&#xff1b;它也是一个新兴独立的开放生态。主框架仅 0.1 MB。 150来个生态插件&#xff0c;覆盖各种不同的应用开发场景&#xff1…

Linux Capabilities 入门

目录 Linux capabilities 是什么&#xff1f; capabilities 的赋予和继承 线程的 capabilities Permitted Effective Inheritable Bounding Ambient 文件的 capabilities Permitted Inheritable Effective 运行 execve() 后 capabilities 的变化 案例 Linux capab…

黑马】后台管理183-

一、添加时间线组件时间线组件在element2.6.0的版本中才可以找到项目中用到的插件早于时间线插件发布的&#xff0c;所以需要用到素材中的&#xff0c;timeline 和timeline-item复制到\code\shop-hou\src\plugins\1,在element.js中导入timeline等组件import Timeline from ./ti…

20230213在AIO-3568J开发板在原厂Android12下跑通ap6275s

20230213在AIO-3568J开发板在原厂Android12下跑通ap6275s 2023/2/13 8:59 一、从AIO-3568的Android11的kernel中抠出来AP6275S的驱动&#xff1a; 1、 Z:\android12-rk3588-new\kernel-4.19\arch\arm64\configs\rockchip_defconfig # CONFIG_WLAN_VENDOR_QUANTENNA is not set …

电路中的过压(OVP)过流(OCP)保护电路

电路中的过压(OVP)过流(OCP)保护电路 我们在学习电路设计的过程中,总会发现电路在过压或者过流时遭到不可逆的损坏, 下面分享几例过压过流保护电路, 希望对朋友们有所帮助, 个人能力有限, 如有不当之处还请多多指教: 例 1&#xff1a;OVP(过压保护) 1).当VCC_IN小于28V时&…

spring(三)-----------什么是beanDefinition?

上篇我们以mybatis如何注入mapper对象为引&#xff0c;发现mybatis使用了FactoryBean&#xff08;动态代理&#xff09;动态注册beanDefinition 的方式实现了对多个bean进行注入。 这篇我们延续上篇的问题&#xff0c;什么是beanDefinition&#xff1f;动态注入beanDefinition…

yum安装

仓库安装[rootlocalhost ~]# rpm -ivh https://repo.mysql.com//mysql80-community-release-el9-1.noarch.rpm[roothezhihao ~]# dnf install mysql-server -y本地安装wget https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.32-1.el9.x86_64.rpm-bundle.tartar xvf mysql…

STK + C# + Cesium联合编程(二):CZML文件生成及加载

概述本文续上一篇博文&#xff0c;上一篇文章中验证了C# .NET Framework (Web Service) STK Cesium前端显示的相关技术&#xff0c;本篇通过STK安装附带的Pro Tutorial实例以及Export CZML插件演示如何创建STK场景&#xff0c;创建对象&#xff0c;计算Access&#xff0c;并通…

【软件测试】8年测试老兵的突破之路,一路升级打怪......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 目前几乎所有的大厂…