高阶数据结构[2]图的初相识

图的初相识

1.前言

2.图的概念

3.图的相关术语

4.图的存储结构 

4.1邻接矩阵 

4.2邻接表 

4.3两种存储方式的对比

 5.图的存储实现

5.1邻接矩阵的实现

5.2邻接表的实现 

6.总结 


1.前言

本章将大家学习数据结构中的“图”。有学习过离散数学的同学对这一章节或许会比较熟悉。本篇文章将从最基础的概念开始介绍,一步步深入。让我们开始吧!

2.图的概念

大家是否还记得我们学过的“树”?“树”就是一种无环的连通图。

图有两个部分组成:1.顶点集合  2.顶点间形成的边。

因此图是一种由顶点集合以及边集合组成的一种数据结构表示为G=(V,E)。

下面我们用图像来理解该概念。

如上图所示。G1中四个顶点0,1,2,3 ,各顶点形成连接的边。抽象概念可以表示为顶点集合v1,v2,v3,v4。形成的边可以表示为ek=(vi,vj),表示改边由顶点vi和vj连接而成。

G2就是我们刚刚介绍的树是一种无向连通图。

 既然说到了无向图,那么必然就有有向图。如图,G3,G4均为有向图。相信大家能看出来有向图和无向图的区别了!无向图没有明确的指向,而有向图则有明确的指向。

3.图的相关术语

在图的学习中,会涉及到大量的专业名词,我们在此一一列举。

\bigstar完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图. 上图的G1就是无向完全图, G4为有向完全图。


\bigstar邻接顶点: 若顶点u和v有直接的边相连, 那么它们这两个顶点就称为邻接顶点。


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

上图中的G3中顶点1的出度为2,入度为1,顶点的度为3.


\bigstar路径:若顶点A可以到达顶点B, 则从A到B经过的所有顶点就是A到B的路径. 对于不带权图,路径长度等于边数之和,带权图则是权值之和。如图。


 \bigstar简单路径和回路:从图上的任一顶点出发,路径上经过的顶点只过一次,则为简单路径。

回路则是路径的上的出发点和结束点为同一个顶点形成一个环。如图。


 \bigstar子图:图2的顶点和边都是图1的一部分 ,则称图2是图1的子图。如图。


 \bigstar连通图:在无向图中,若两顶点间有边则称这两个边连通。若任意两个顶点都有边,则称为连通图。


\bigstar强连通图:在有向图中,任意两个顶点间都有相连的边,并且顶点1有指向顶点2,顶点2也指向1。


\bigstar生成树:一个无向连通图的最小连通子图称为改图的生成树。n个顶点的连通图的生成树有n-1条边。

4.图的存储结构 

图的存储结构有两种:1.邻接矩阵 2.邻接表

4.1邻接矩阵 

 邻接矩阵用以判断顶点间是否连通,用0来表示连通,1来表示不连通。

因此,我们用二维数组来实现邻接矩阵,表示两点是否连接。

 如上图,对于无向图,矩阵是对称的。因为,A对于B是连通的,B对于A也是连通的。

对于有向图而言,如图,二维数组的行表示边是由该顶点出发,而列则为被指向的顶点。

若图中的边带有权值,则在二维矩阵中用无穷大来表示两顶点间无边即不连通。

4.2邻接表 

邻接表:用数组表示顶点的集合,用链表表示边的关系。

无向图的邻接表

 假设顶点A,B,C,D 的下标为0,1,2,3。与A连通的有B,C。所以在邻接表中,A指向B,C。

有向图的邻接矩阵 

有向图的邻接表中包含有入边表和出边表。以A为例,顶点E指向A因此在入边表中A连接E的下标4。在出边表中,由于从A出的边指向B和D,所以出边表指向B和D的下标1,3。 

 4.3两种存储方式的对比

而邻接表的优点是很快能判断出一个顶点与哪些顶点直接相连. 而邻接表想要知道两个顶点是否连通,要比邻接矩阵要麻烦。

 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求。

 5.图的存储实现

 首先,和学习其他数据结构一样。我们先来实现图的基本框架。上代码。

template<class V,class W,W MAX_W=INT_MAX,bool Direction=false>//V表示顶点的类型 W表示边的类型,MAX_W作为二维数组的初始值
class Graph {
piblic:
	Graph(const V* a, size_t n)
	{
		_vertrx.resize(n);//为顶点集合开辟空间
		for (int i = 0; i < n; i++)
		{
			_vertex.push(a[i]);//将数组中的值写入集合
			_indexMap[a[i]] = i;//顶点值和顶点下标的映射
		}
		_edge.resize(n);//为边的集合开辟空间
		for (int i = 0; i < n; i++)
		{
			_edge[i].resize(n, MAX_W);
		}
	}
private:
	vector<v> _vertex;//存储顶点的集合
	vector<vector<w>>edge;//存储边的集合
	map<v, w>_indexMap;//存储顶点和其映射
};

5.1邻接矩阵的实现

	namespace martix {
		template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
		class Graph {
		public:
			Graph(const V* a, size_t n)
			{
				_vertex.reserve(n);
				for (int i = 0; i < n; i++)
				{
					_vertex.push_back(a[i]);
					_indeMap[a[i]] = i;
				}
				_edge.resize(n, MAX_W);
			}
			size_t GetIndex(const V& v)
			{
				if (_indexMap.find(v) == _index.end())//map的特性,找不到时就等于end
				{
					cout << "要添加的顶点不存在" << endl;
					return -1;
				}
				return _index[v];
			}
			void AddEdge(const V& src, const V& dest, const W& w)//加边,依次为源点,目标点和边的权值
			{
				size_t srci = GetIndex(src);
				size_t desti = GetIndex(dest);
				_edge[srci][desti] = w;//边的权值
				if (Direction = false)
				{
					_edge[desti][srci] = w;//无向图,矩阵是对称的
				}
				void print()
				{
					//打印顶点
					for (int i = 0; i < _edge.size(); i++)
						cout << "[" << i << "]" << "->" << _vertex[i] << endl;
					cout << endl;
					//打印矩阵
					for (int i = 0; i < _edge.size(); i++)//横坐标
					{
						for (int j = 0; i < _edge[i]).size(); j++)//纵坐标
						{
						if (_edge[i][j] = MAX_W)
							cout << "*";
						else {
							cout << _edge[i][j] << " ";
						}
}
					}
					cout << endl;
				}
				cout << endl;
			}
		};
	private:
		vector<V>_vertex;//顶点的集合
		vector<vector<w>> _edge;//边的集合
		map<v, w>_indexMap;
	}

5.2邻接表的实现 

	namespace link_table
	{
		template<class W>//边的权值
		struct Edge {
			int _dsti;//目标点的下标
			W _w;
			Edge<W>* _next;
				Edge(int dsti, constW& w)//邻接表用链表的形式实现,因此是对每一个结点进行初始化
				:_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)
			{
				_vertex.reverse(n);//顶点集合开辟空间
				for (int i = 0; i < n; i++)
				{
					_vertex.push_back(a[i]);
					_indexMap(a[i]) = i;
				}
				_tables.resize(m, nullptr);
			}
			size_t GetVertexIndex(const V& v)
			{
				size_t it = _indexMap.find(v);
				if (it != _indexMap.end())
				{
					return it->second;//返回顶点集合映射的下标
				}
				else {
					return -1;
				}
			}
			void AddEdge(const V& src, const V& dst, const W& w)
			{
				size_t srci = GetVertexIndex(src);
				sizet_t dsti = GetVertexIndex(dst);
				Edge* eg = new Edge(dsti, w);//对结点进行开辟
				eg->_next = _tables[srci];//头插的方式加入邻接表的新边,指向原来的头
				_tables[srci] = eg;//做新头
				if (Direction == false)
				{
					Edge* eg = new Edge(srci, w);
					eg->_next;_tables[dsti];
					_tables[dsti] = edge;//做新头
				}
			}
			void Print()
			{
				for (size_t i = 0; i < _vertex.size(); i++)
				{
					cout << "[" << i << "]" << "->" << _vertex[i] << endl;//下标对应的顶点
				}
				cout << endl;
				for (size_t i = 0; i < _tables.size(); i++)
				{
					cout << _vertex[i] << "[" << i << "]" << "->";
					Edge* cur = _tables[i];
					while (cur)//和链表一样的遍历方式
					{
						cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
						cur = cur->_next;
					}
				}
			}
		private:
			vector<V>_vertex;//顶点的集合
			map<V, int>_indexMap;
			vector<Edge*>_tables;
		};
	
	}

可以看到,邻接矩阵和邻接表在实现时的差别,邻接矩阵使用二维数组,而邻接表则使用的链表。

此外,在邻接表中插入新边时,我们使用的是头插的方式,因为这样可以免去找链表尾的操作。 

6.总结 

本篇文章,我们对图进行了基本的了解。在后续讲解图的相关算法时,我们将以邻接矩阵的存储方式的基本框架来分析。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值