【C++】深入浅出“图”——图的基本概念与存储结构


各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多三连分享交流,一起学习进步!

欢迎关注我的blog主页: 落羽的落羽

文章目录

一、图的基本概念

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

  • V是顶点集合
  • E是顶点间关系的集合,也叫做边的集合
  • 图中的结点称为顶点。如果两个顶点 vi 和 vj 之间相关联,称有一条边记<vi, vj>

图分为有向图和无向图:在有向图中,<x, y>称为顶点x到顶点y的一条边,<x, y>和<y, x>是两条不同的边。而在无向图中,<x, y>和<y, x>是同一条边。

图中边的数量与顶点数量的关系,可以用稠密和稀疏形容。

完全图:设一张图有n个顶点。对于无向图,若有n*(n-1)/2条边,即任意两点之间都有一条边,则称此图为无向完全图,也是最稠密的图;对于有向图,若有n*(n-1)条边,即任意两点之间都有两条方向相反的边,则称此图为有向完全图。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点v的度等于该顶点的入度与出度之和,入度是指以v为终点的有向边的条数,出度是指以v为起点的有向边的条数。在无向图中,顶点v的度等于出度等于入度。

路径:在图中,若从顶点 vi 出发有一组边可以使其到达顶点 vj,则称顶点 vi 到 vj 的顶点序列为顶点 vi 到 vj 的路径。

权值:边附带的数据信息,比如:长度,价值,亲密度

路径长度:对于不带权的图,一条路径的路径长度是该路径上的边数量;对于带权的图,一条路径的路径长度是该路径上的所有边的权总和。

简单路径与回路:若一条路径上的各顶点都不重复,则称这样的路径为简单路径。若路径上的第一个顶点和最后一个顶点重合,则称这样的路径为回路或环。


子图:设图G = {V, E}、G1 = {V1, E1},若V1属于V且E1属于E,则称G1是子图。即一个图的子图,所有的顶点和边都在原图中出现过。

连通图:对于无向图,若顶点v1到v2有路径,则称v1和v2是连通的。如果图中任意两个顶点都是连通的,则称此图为连通图。

强连通图:对于有向图,如果任意两顶点vi和vj之间,都存在一条vi到vj的路径和vj到vi的路径,则称此图为强连通图。

生成树:生成树是连通图(无向图)的一个子图,是一棵树,包含原图所有顶点且保持连通,有n个顶点的连通图的生成树有n个顶点和n-1条边。

二、图的存储结构

一个图既有顶点也有边,图的存储结构中就需要保存它们。顶点保存比较简单,只需要一个数组即可,关系边该怎么保存呢?

保存边的方式,有领接矩阵和领接表两种方式!

1. 领接矩阵

顶点与顶点之间是否连通,可以用0或1表示。领接矩阵就是一个二维数组,用矩阵来表示顶点之间的关系

每个结点可以用数组下标代表。例如,顶点A的下标是x,顶点B的下标是y。领接矩阵中[x][y]代表从A到B的边的权值,如果是无权图就用01表示该边是否存在即可,如果是有权图则填入权值或默认值(表示边不存在,一般可以用INT_MAX代表)。
注意,对于无向图,领接矩阵是左下右上对称的,即[x][y][y][x]内容一样!有向图则不是,[x][y][y][x]内容不一定相同。

cpp 复制代码
// 图
namespace Matrix
{
	// V顶点类型 W边权值类型 MAX_W表示边不存在的值 Direction表示图是否有向
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
	public:

		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 作为不存在边的标识值
			// 初始化时默认没有边,边需要一条一条手动添加,用AddEdge函数
			_matrix.resize(n);
			for (auto& e : _matrix)
			{
				e.resize(n, MAX_W);
			}
		}

		// 找到一个顶点的映射下标
		size_t GetVertexIndex(const V& v)
		{
			auto ret = _vIndexMap.find(v);
			if (ret != _vIndexMap.end())
			{
				return ret->second;
			}
			else
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
		}

		// 添加一条边,src和dst代表两端顶点,w是权值
		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;
			//如果是无向图,则[dsti][srci]也需添加边
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}

	private:
		map<V, size_t> _vIndexMap;   // 顶点到下标的映射
		vector<V> _vertexs;			 // 顶点集合
		vector<vector<W>> _matrix;   // 领接矩阵 存储边
	};

}

领接矩阵适合存储稠密图,能O(1)判断两个顶点的关系,得到权值。但是如果要查找一个顶点连接的所有边,效率是O(n)

2. 领接表

领接表是一个链表数组。数组表示顶点的集合,链表表示边的关系。

领接表适合存储稀疏图,适合查找一个顶点连接出去的边,但是相对不适合判断两个点是否有边及其权值。

cpp 复制代码
// 临接表
namespace LinkTable
{
	// 定义边结构, W是权值类型
	template<class W>
	struct LinkEdge
	{
		int _srcIndex;
		int _dstIndex;
		W _w;
		LinkEdge<W>* _next;

		LinkEdge(const W& w)
			: _srcIndex(-1)
			, _dstIndex(-1)
			, _w(w)
			, _next(nullptr)
		{ }
	};

	template<class V, class W, bool Direction = false>
	class Graph
	{
		typedef LinkEdge<W> Edge;
	public:

		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;
			}

			_linkTable.resize(n, nullptr);
		}

		size_t GetVertexIndex(const V& v)
		{
			auto ret = _vIndexMap.find(v);
			if (ret != _vIndexMap.end())
			{
				return ret->second;
			}
			else
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
		}

		// 添加边
		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srcindex = GetVertexIndex(src);
			size_t dstindex = GetVertexIndex(dst);

			Edge* sd_edge = new Edge(w);
			sd_edge->_srcIndex = srcindex;
			sd_edge->_dstIndex = dstindex;
			sd_edge->_next = _linkTable[srcindex];
			_linkTable[srcindex] = sd_edge;

			// 如果是无向图,还要反过来添加一次
			if (Direction == false)
			{
				Edge* ds_edge = new Edge(w);
				ds_edge->_srcIndex = dstindex;
				ds_edge->_dstIndex = srcindex;
				ds_edge->_next = _linkTable[dstindex];
				_linkTable[dstindex] = ds_edge;
			}
		}


	private:
		map<string, int> _vIndexMap; // 顶点到下标的映射
		vector<V> _vertexs;			 // 顶点集合
		vector<Edge*> _linkTable;    // 边的集合的领接表
	};

}

本篇完,感谢阅读

相关推荐
bugcome_com2 小时前
C# 中 Overload(重载)与 Override(重写)的核心区别与实战解析
开发语言·c#
DatGuy2 小时前
Week 30: 机器学习补遗:时序信号处理与数学特征工程
人工智能·机器学习·信号处理
秋深枫叶红2 小时前
嵌入式第三十九篇——linux系统编程——信号通信、共享内存
linux·运维·服务器·学习
IT方大同2 小时前
循环结构的功能
c语言·数据结构·算法
摸鱼仙人~2 小时前
大语言模型微调中的数据分布不均与长尾任务优化策略
人工智能·深度学习·机器学习
巴塞罗那的风2 小时前
从蓝图到执行:智能体中的“战略家思维
开发语言·后端·ai·语言模型·golang
JAVA+C语言2 小时前
Python新手学习
开发语言·python·学习
乌萨奇也要立志学C++2 小时前
【Linux】线程互斥与互斥量全解析:原理、实践与封装
linux·服务器
LeeZhao@2 小时前
【狂飙全模态】狂飙AGI-Wan2.1文生视频实战部署-Gradio篇
人工智能·语言模型·音视频·agi