数据结构 之 【图】(图的基本概念与图的存储结构:邻接矩阵、邻接表)

目录

1.图的基本概念

2.图的存储结构

[2.1 邻接矩阵](#2.1 邻接矩阵)

[2.2.1 邻接矩阵的简介](#2.2.1 邻接矩阵的简介)

[2.1.2 邻接矩阵的简单实现](#2.1.2 邻接矩阵的简单实现)

[2.2 邻接表](#2.2 邻接表)

[2.2.1 邻接表的简介](#2.2.1 邻接表的简介)

[2.2.2 邻接表的简单实现](#2.2.2 邻接表的简单实现)


1.图的基本概念

  • 图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:顶点集合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中,若是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶 点u,并称边与顶点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 邻接矩阵

2.2.1 邻接矩阵的简介

因为节点与节点之间的关系就是连通与否,即为0(不连通)或者1(连通)或者附带权值(连通),

因此邻接矩阵(二维数组)即是:

先用一个数组将顶点保存,然后采用矩阵来表示节点与节点之间的关系

注意:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。

有向图的邻接矩阵不一 定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。

  1. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,

如果两个顶点不通,则使用无穷大代替

  1. 邻接矩阵存储图

优点是能够快速知道O(1)两个顶点是否连通,适合稠密图

缺陷 是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间 ,并且不是很好求两个节点之间的路径

2.1.2 邻接矩阵的简单实现

复制代码
namespace matrix
{
	//邻接矩阵
	template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>
	class graph
	{
	public:
		//手动初始化
		graph(const V* a, size_t n){}

		int GetIndexOfVertexes(const V& v){}

		void AddEdge(const V& src, const V& dst, const W& w){}

		void Print(){}

	private:
		vector<V> _vertexes;//顶点集合
		unordered_map<V, int> _index;//顶点映射下标
		vector<vector<W>> _matrix;//邻接矩阵存放权值
	};

(1)创建命名空间,防止与后续的邻接表实现发生命名冲突

(2)模板参数中,V是顶点类型,W是权值类型,权值默认给INT_MAX,默认无向图

(3)三个成员:_vertexes 存储顶点,_index 映射顶点对应的下标,_matrix 就是邻接矩阵了

(4)成员函数:

这里为了方便测试,选择构造函数手动初始化顶点集合等

另外的重点就是通过给定两顶点,添加边以及为了方便观察,打印邻接矩阵

  • 构造函数

    graph(const V* a, size_t n)
    {
    _vertexes.reserve(n);
    for (int i = 0; i < n; ++i)
    {
    _vertexes.push_back(a[i]);
    _index[a[i]] = i;
    }
    //n行n列全部初始化为最大值
    _matrix.resize(n, vector<int>(n, W_MAX));
    }

依据顶点数组及其元素个数,初始化顶点集合及映射关系

邻接矩阵初始化为n行n列,每条边对应的权值都是最大值

  • 添加边

    int GetIndexOfVertexes(const V& v)
    {
    auto it = _index.find(v);
    if (it == _index.end())
    throw invalid_argument("无效参数");

    复制代码
      return it->second;

    }

为了方便后续通过顶点直接找到边,这里单独写一个函数返回顶点所对应的下标

(1)首先,判断所给顶点的有效性,即在映射关系中查找顶点是否存在

(2)存在即返回下标,不存在就及时抛异常

复制代码
void AddEdge(const V& src, const V& dst, const W& w)
{
	//得到起点和终点的下标
	int srci = GetIndexOfVertexes(src);
	int dsti = GetIndexOfVertexes(dst);
	//标识矩阵中对应元素
	_matrix[srci][dsti] = w;
	//无向图
	if (Direction == false)
	{
		_matrix[dsti][srci] = w;
	}
}

(1)对于给定的两个顶点,先找到其对应的下标

(2)然后根据始末位置找到边所对应的矩阵元素进行赋值操作

(3)如果是无向图,再到对称位置的矩阵元素进行赋值

  • 打印

    复制代码
      	void Print()
      	{
      		//打印 下标及其对应的顶点值
      		for (int i = 0; i < _vertexes.size(); ++i)
      		{
      			cout << '[' << i << "] -> " << _vertexes[i] << endl;
      		}
      		//横下标
      		cout << setw(6) << 0;
      		for (int i = 1; i < _matrix.size(); ++i)
      		{
      			cout << setw(5) << i;
      		}
      		cout << endl;
      		for (int i = 0; i < _matrix.size(); ++i)
      		{
      			//列下标
      			cout << i;
      			for (int j = 0; j < _matrix[i].size(); ++j)
      			{
      				//矩阵元素
      				if(_matrix[i][j] != W_MAX)
      					cout << setw(5) << _matrix[i][j];
      				else cout << setw(5) << '*';
      			}
      			//打印完一行要换行
      			cout << endl;
      		}
      	}

(1)先打印下标及下标对应的顶点值

(2)再打印横下标,这里使用setw控制数据宽度(右对齐)

(3)对于矩阵的每一行,先打印行数,再打印权值,并且为了好看,当两顶点无边,即权值为

INT_MAX时,我们选择打印 '*'

2.2 邻接表

2.2.1 邻接表的简介

  • 邻接表:使用数组表示顶点的集合(数组下标直接对应顶点编号),使用链表表示边的关系
  • 将边视为一个结构体,存储起点、终点、权值、指向下一条边的指针
  • 类似于哈希桶,起点的数组下标决定了边在邻接表中的挂载位置
  • 注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点 vi边链表集合中结点的数目即可

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就 是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链 表,看有多少边顶点的dst取值是i
邻接表存储图

优点是能够更快查找一个顶点连接出去的边,适合稀疏图

缺陷不适合确定两个顶点是否相连及其权值

2.2.2 邻接表的简单实现

复制代码
namespace link_table
{
	template<class W>
	struct Edge
	{
		int _srci;//起点
		int _dsti;//终点
		W _w;	 //权值
		Edge<W>* _next;//连接到邻接表中去

		Edge(int srci, int dsti, const W& w)
			:_srci(srci),_dsti(dsti),_w(w)
			,_next(nullptr)
		{ }
	};
	//邻接表
	template<class V, class W, bool Direction = false>
	class graph
	{
		typedef Edge<W> Edge;

	public:
		graph(const V* a, size_t n){}

		int GetIndexOfVertexes(const V& v){}

		void AddEdge(const V& src, const V& dst, const W& w){}

		void Print(){}

	private:
		vector<V> _vertexes;//顶点集合
		unordered_map<V, int> _index;//顶点映射下标
		vector<Edge*> _tables;//邻接表 存放边的集合
	};
}

(1)创建命名空间,防止与后续的邻接表实现发生命名冲突

(2)正如前面介绍的一样,将边视为一个结构体

存储起点、终点、权值、指向下一条边的指针

(3)图的模板参数就不用权值的最大值了

(3)三个成员:_vertexes 存储顶点,_index 映射顶点对应的下标,_tables 就是邻接表

这里只实现了出度表

(4)成员函数:

这里为了方便测试,选择构造函数手动初始化顶点集合等

另外的重点就是通过给定两顶点,添加边以及为了方便观察,打印邻接表

  • 构造函数

    graph(const V* a, size_t n)
    {
    _vertexes.reserve(n);
    for (int i = 0; i < n; ++i)
    {
    _vertexes.push_back(a[i]);
    _index[a[i]] = i;
    }

    复制代码
      _tables.resize(n);

    }

邻接表顶点集合与下标映射关系的初始化与邻接矩阵一致

邻接表的_tables初始化顶点个(n个)空间

  • 添加边

    void AddEdge(const V& src, const V& dst, const W& w)
    {
    //得到起点和终点的下标
    int srci = GetIndexOfVertexes(src);
    int dsti = GetIndexOfVertexes(dst);
    //构造边
    Edge* eg = new Edge(srci, dsti, w);
    //头插
    eg->_next = _tables[srci];
    _tables[srci] = eg;
    if (Direction == false)
    {
    //构造边
    Edge* eg = new Edge(dsti, srci, w);
    //头插
    eg->_next = _tables[dsti];
    _tables[dsti] = eg;
    }
    }

(1)这里根据顶点获得下标的函数与邻接矩阵的一致,这里不再赘述

(3)获得下标后,根据始末位置及权值构造边

(3)单链表头插效率更高

(4)如果是无向图,则还需要链接一次

  • 打印

    复制代码
      	void Print()
      	{
      		//打印 下标及其对应的顶点值
      		for (int i = 0; i < _vertexes.size(); ++i)
      		{
      			cout << '[' << i << "] -> " << _vertexes[i] << endl;
      		}
      		cout << endl;
      		//打印邻接表
      		for (int i = 0; i < _tables.size(); ++i)
      		{
      			//打印起点
      			cout << "[ " << _vertexes[i] << " : " << i << " ] -> ";
      			//起点对应的边
      			Edge* cur = _tables[i];
      			while (cur)
      			{
      				cout << "[ " << _vertexes[cur->_dsti] << " : " 
      					<< cur->_dsti << " : " << cur->_w << " ]" << " -> ";
    
      				cur = cur->_next;
      			}
    
      			cout << "nullptr" << endl;
      		}
    
      	}

(1)先打印下标及下标对应的顶点值

(2)针对邻接表的每个位置

先打印起点:下标,再打印起点到达的位置

相关推荐
爱和冰阔落3 小时前
【C++list】底层结构、迭代器核心原理与常用接口实现全解析
开发语言·数据结构·c++·list
Ms.lan3 小时前
C++数组
数据结构·c++·算法·visual studio
~~李木子~~3 小时前
归并排序算法
数据结构·算法·排序算法
papership12 小时前
【入门级-算法-6、排序算法: 计数排序】
数据结构·算法·排序算法
2401_8401052012 小时前
GESP C++5级 2025年6月编程2题解:最大公因数
数据结构·c++·算法
myw07120514 小时前
Leetcode94.二叉数的中序遍历练习
c语言·数据结构·笔记·算法
songx_9914 小时前
leetcode(填充每个节点的下一个右侧节点指针 II)
java·数据结构·算法·leetcode
chenyuhao202414 小时前
vector深度求索(上)实用篇
开发语言·数据结构·c++·后端·算法·类和对象
@Zeker15 小时前
并查集(Union-Find)数据结构详解
数据结构