c++高级数据结构之图(基本概念,存储结构,BFS,DFS,最小生成树)

一.概念

注:

几个简称字母的翻译:

G:graph,图表

V:vertex,顶点

E:edge,边


1.图的两要素

由上文可知,图由顶点和边 这两要素组成。而下图的边是一个双向通路。

当然,边上还需有权值,即边附带的数据信息。如下图这样:

2.有向与无向

3.完全图(重要)

4.邻接顶点(半重要)

5.顶点的度

例子:5的两个度:

6.路径

7.简单路径与回路

注:递归的时候一定要避免出现回路,不然会递归死循环!!!!!

8.子图(重要)

子图的顶点和边都是原图的子集。

9.连通图和生成树

二.图的存储结构

1.储存顶点

直接用一个**vector(_vertexs)**作为顶点集,储存顶点即可:

2.储存边

此时就有人说,用一个pair<V,V>就可以存下边的两个顶点,进而就能代表存储一条边了,但这是不可以的。因为忽略了无法用pair表示的细节:

下面来叙述正确的存储方式:

①.邻接矩阵(该存储方式适合稠密图)

注:以矩阵的行代表方向的起点,列代表终点。

相关特点

Ⅰ.由于用矩阵表示时,表示顶点的是其存储在_vertexs的下标,因此要用map建立一个顶点与其下标的映射关系:

Ⅱ.如下图的矩阵,如果没有权值 ,某条边对应两个顶点有关系(相连), 那么矩阵中对应位置的值为1。

Ⅲ.那么两个顶点相连(边存在 ),且有权值 的情况呢,答案是矩阵对应的值不再是1,而是权值 ,而边不存在,矩阵就存个标识值(0,∞等)

Ⅳ.上面两个图一个是无向图,一个是有向图,那么它们在矩阵中存储时有何区别呢?

无向图

中轴线 为对称轴,无向图的存储矩阵是个对称矩阵:

毕竟无向图边的方向是双向的,(A,B)与(B,A)是同一条边,所以A到B与B到A都存在,且矩阵上对应值也是相同的。

有向图

而有向图由于有方向,所以有<1,3>,不代表有<3,1>,所以无法形成以中轴线为轴的对称。

优劣性
代码实现

在看代码之前先明晰一点,map的find接口返回的规则是:找得到这个key,就返回指向存储key键值对(pair)的迭代器,找不到,就返回end()迭代器。


复制代码
#pragma once
#include<vector>
#include<map>
namespace matrix
{//邻接矩阵版本
	template<class V, class W, W MAX_W = INT_MAX ,bool Direction = false>//用V表示顶点类型,用W,即weight表示权值,Direction这个非类型模板参数代表有无向
	class Graph
	{
	public:
		Graph(const V* a,size_t n)
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);//存顶点
				_indexMap[a[i]] = i;//存映射值
			}
			_matrix.resize(n);//先开n列
			for (int i = 0; i < _matrix.size(); i++)
			{
				_matrix[i].resize(n, MAX_W);//再开n行,并用MAX_W初始化
			}
		}
		size_t GetVertexIndex(const V& v)//获取顶点下标
		{
			//先获取到的顶点是否就是这个图的顶点
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}
		void AddEdge(const V& src,const V& des,const W& w )
		{
			size_t srci = GetVertexIndex(src);
			size_t desi = GetVertexIndex(des);
			//连接首尾,添加边
			_matrix[srci][desi] = w;
			//无向图
			if (Direction == false)
			{
				_matrix[desi][srci] = w;
			}
		}
		void Print()
		{
			//打印顶点
			for (int i = 0;i < _vertexs.size();++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;
			//打印矩阵
			cout << "  ";
			for (int i = 0; i < _matrix.size(); i++)
			{
				cout << i << " ";//打印列标
			}
			cout << endl;
			for (int i = 0; i < _matrix.size(); i++)
			{
				cout << i << " ";//打印行号
				for (int j = 0; j < _matrix[i].size(); ++j)
				{
					//cout << _matrix[i][j] << " ";
					if (_matrix[i][j] == MAX_W)
					{
						cout << "* ";
					}
					else
					{
						cout << _matrix[i][j] << " ";
					}
				}
				cout << endl;
			}
			cout << endl;
		}
	private:
		vector<V> _vertexs;//用vector存储所有顶点,即_vertexs为顶点集合
		map<V, int> _indexMap;//储存顶点映射的下标
		vector<vector<W>> _matrix;//矩阵
	};
	void TestGraph()
	{
		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();
	}
}

②.邻接表(适合存储稀疏图)

相关特点
无向图
有向图

注:有向图存一个出边表即可

优劣性
代码实现
复制代码
	template<class V, class W, bool Direction = false>//用V表示顶点类型,用W,即weight表示权值,Direction这个非类型模板参数代表有无向
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);//存顶点
				_indexMap[a[i]] = i;//存映射值
			}
			_tables.resize(n,nullptr);//邻接表开n个空间
		}
		size_t GetVertexIndex(const V& v)//获取顶点下标
		{
			//先获取到的顶点是否就是这个图的顶点
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}
		void AddEdge(const V& src, const V& des, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t desi = GetVertexIndex(des);
			//1->2
			Edge* eg = new Edge(desi,w);
			//头插,先把数组元素给next,再让eg成为数组元素
			eg->_next = _tables[srci];
			_tables[srci] = eg;
			if (Direction == false)
			{//如果是无向图
				//2->1
				Edge* eg = new Edge(srci, w);
				eg->_next = _tables[desi];
				_tables[desi] = eg;
			
			}
		}
		void Print()
		{
			//打印顶点
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;
			
		}
	private:
		vector<V> _vertexs;//用vector存储所有顶点,即_vertexs为顶点集合
		map<V, int> _indexMap;//储存顶点映射的下标
		vector<Edge*> _tables;//邻接表
	};
	
}

三.图的遍历

注:遍历是指遍历图的顶点。

1.广度优先遍历(BFS)

①.算法说明

从第0层的A开始,自己先进队列,随后出队列,同时将第1层跟自己相关联 的几个元素带进队列,随后第1层的元素在出队列的同时,会把跟自己相关联的,无论哪一层的元素都带进队列,这就会把之前已经入过队列的上一层元素再带进来,导致重复。解决重复的方案就是再开一个数组,去标记已经进过上面队列的元素,待出现重复元素时,就不会让其进队列。

这样就能做到逐圈向外遍历。

②.代码实现

标记数组的下标就映射了具体的人名,每个数组元素无非有两个答案:true/false,当某个人初次进入队列,标记数组所对应该人的那个元素就会变成true。

代码

这里主要的难点是把抽象的图形如何具象化,有很多个存储图组成元素的容器,非常容易弄混。

复制代码
void BFS(const V& src)
{
	size_t srci = GetVertexIndex(src);//获取下标
	//队列and标记数组
	queue<int> q;
	vector<bool> visited(_vertexs.size(), false);
	//入队列
	q.push(srci);
	visited[srci] = true;
	size_t n = _vertexs.size();
	while (!q.empty())
	{
		int front = q.front();
		q.pop();
		cout << front << ":" << _vertexs[front];
		for (int i = 0; i < n; ++i)//将front的裙带关系带进队列
		{
			if (_matrix[front][i] != MAX_W)
			{
				if (visited[i] == false)
				{
					q.push(i);
					visited[i] = true;
				}
			
			}
		}
	}
	cout << endl;
}
测试用例与结果

③.优化------逐层出队列------应对几度好友美团笔试题

此处还是基于上面张三,李四,王五的图进行优化:

代码:

复制代码
void BFS(const V& src)
{
	size_t srci = GetVertexIndex(src);//获取下标
	//队列and标记数组
	queue<int> q;
	vector<bool> visited(_vertexs.size(), false);
	//入队列
	q.push(srci);
	visited[srci] = true;
	size_t n = _vertexs.size();
	int levelSize = 1;
	while (!q.empty())
	{
		for (size_t i = 0; i < levelSize; ++i)//多一轮循环,控制逐层出队列
		{
			int front = q.front();
			q.pop();
			cout << front << ":" << _vertexs[front];
			for (int i = 0; i < n; ++i)//将front的裙带关系带进队列
			{
				if (_matrix[front][i] != MAX_W)
				{
					if (visited[i] == false)
					{
						q.push(i);
						visited[i] = true;
					}

				}
			}
		}
		cout << endl;
		levelSize = q.size();
	}
}
优化后的结果:

2.深度优先遍历(DFS)

遍历的图是连通图

递归展开图:

复制代码
void _DFS(size_t srci, vector<bool>& visited)
{
	cout << srci << ":" << _vertexs[srci] << endl;
	visited[srci] = true;
	for (int i = 0; i < _vertexs.size(); i++)
	{
		if (_matrix[srci][i] != MAX_W && visited[srci] = false)
		{
			_DFS(i, visited);
		}
	}
}
void DFS(const V& src)
{
	size_t srci = GetVertexIndex(src);
	vector<bool> visited(_vertexs.size(), false);
	_DFS(srci, visited);
}

四.最小生成树

1.Kruskal算法

①.算法原理

并查集不清楚的可参见笔者c++高级数据结构之并查集

用并查集去判断成环与否:

图G就是森林A增长的最终结果,如上图的图n。

②.代码实现

在找当下权值最小的边时 ,会把所有边扔进priority_queue进行查找 ,如果priority_queue不太清楚,请参见笔者栈和队列的实现这篇文章。

while循环的条件误区:

我们需要选出size =(n - 1)条边,因此要用while循环,但我们的目标是从图的所有边里,找到n - 1条边,因此while循环当是针对图的图的边数肯定是大于 n - 1,因此假如是size--,意思就是从图的n - 1条边里,找n - 1条边,这肯定是找不全的,因此while的条件打死不能是size--。

代码
复制代码
W Kruskal(Self& minTree)
{
	size_t n = _vertexs.size();
	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	minTree._matrix.resize(n);
	for (size_t i = 0;i < n;++i)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}
	priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n; ++j)
		{
			if (_matrix[i][j] != MAX_W && i < j)//i < j可以确保无向图的边不重复
			{
				minque.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}
	//选n-1条边
	size_t size = n - 1;
	UnionFindSet ufs(n);//用其判断成环与否
	W totalW = W();
	while (!minque.empty())
	{
		Edge min = minque.top();
		minque.pop();
		if (!ufs.InSet(min._srci, min._dsti))
		{//没有成环
			minTree._AddEdge(min._srci, min._dsti, min._w);
			ufs.Union(min._srci, min._dsti);
			++size;
			totalW += min._w;
		}
	}
	if (size == n - 1)
	{//n - 1条边找完
		return totalW;
	}
	else
	{//队列走完,依旧找不到n - 1条边
		return W();//返回缺省值
	}
}

2.Prim算法

①.算法原理

将某个端点作为起点,并将其放入集合X中,然后图剩下的端点就归入集合Y中,从Y中每次挑一个与X中端点相邻 的端点,使得**{X} ->{Y}的边权值最小** ,随后将这个端点归入集合X 中,并将其从Y中抹除。

这是一个局部的贪心算法,基于集合X里的端点,在集合Y里挑另一个端点,构成权值最小的边。这波操作下来,就不会出现构成回路的情况。 因为想要构成回路 ,那条边的两个顶点都是出自同一集合。

②.代码实现

先用两个vector代表集合X,Y,去存储端点。

为了效率,用优先级队列minq去存储边,当每个端点进入集合X时,就将与其相连的几条边存进minq里,然后筛选出权值最小的那条边(minq.top),将这条边添加进最小生成树minTree,最后将这条边pop出minq。

但这种实现方式的局限在于,它有些违背了不构成回路的初心,将与X中端点相连的边添加进minq,会把构成回路的边也添加进来,当这条边的权值在局部最小时,会被选中进入minTree,这时就违反了最小生成树不能出现回路的原则:

那么会导致成环的边有什么特征呢?答案是它们均属于集合X,而原算法形成的合法边的两个端点,是来自于不同集合X与Y的,所以原算法可以不必考虑成环,但这里无法规避。

所以在实现层面,我们还得添加防止成环的算法。

代码:

复制代码
W Prim(Self& minTree,const W& src)//src为起点
{
	//与上面算法一样,先对矩阵进行初始化
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();
	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	minTree._matrix.resize(n);
	for (size_t i = 0; i < n; ++i)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}
	//用两个vector作为集合
	vector<bool> X(n,false);
	vector<bool> Y(n,true);
	X[srci] = true;
	Y[srci] = false;
	//去寻找权值最小的边
	priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
	for (size_t i = 0; i < n; ++i)
	{//将与src(a)相连的边放进minq
		if (_matrix[srci][i] != MAX_W)
		{
			minq.push(Edge(srci, i, _matrix[srci][i]));
		}
	}
	int size = 0;
	W totalW = W();//权值
	while (!minq.empty())
	{//取权值最小的边
		Edge min = minq.top();
		minq.pop();
		if (X[min._srci] && X[min._dsti])
		{//判断是否形成回路的算法
			cout << "构成环:";
			cout << _vertexs[min._srci] <<"->" << _vertexs[min._dsti] << ":" << min._w << endl;
		}
		else
		{
			minTree._AddEdge(min._srci, min._dsti, min._w);
			X[min._dsti] = true;//b进X
			Y[min._dsti] = false;//b出Y
			++size;
			totalW += min._w;
			if (size == n - 1)
			{//选n - 1条边
				break;
			}
			for (size_t i = 0; i < n; ++i)
			{//让与b连接的边进入minq
				if (_matrix[min._dsti][i] != MAX_W && X[i] == 0)//有了ab边,那ba边就重复了,去重就是让与b相邻的端点(此时b在X里了)不出现在X集合里
					minq.push(Edge(srci, i, _matrix[min._dsti][i]));
			}
		}
	
	}

	if (size == n - 1)
	{//n - 1条边找完
		return totalW;
	}
	else
	{//队列走完,依旧找不到n - 1条边
		return W();//返回缺省值
	}
}
相关推荐
Tongzhi20261 小时前
考勤数据本地化存储:通芝科技无感考勤一体机的数据安全逻辑
大数据·数据结构·科技·均值算法·数据库开发·推荐算法
c238562 小时前
MySQL 基础用法(下):查询进阶与核心特性
android·c语言·c++·mysql
码匠许师傅2 小时前
【C++ 面试真题】 C++ 中的 static 有什么作用?
c++·面试
郭涤生2 小时前
C++ 零拷贝(Zero-Copy)
linux·开发语言·c++
她说可以呀2 小时前
Spring-ai-Rag-多文件格式生成 List<Document>
数据结构·list
乐观勇敢坚强的老彭2 小时前
计算机内存中的堆和栈
c++
间歇性努力持续性发呆的野生快乐选手3 小时前
dfs回溯,bfs枚举,完全背包
c++·算法·深度优先·宽度优先
郝学胜_神的一滴3 小时前
干货版《算法导论》17:双数极值配对与卡牌手牌编码最优解
数据结构·算法
码工许师傅4 小时前
一文读懂《Effective C++ 第三版》的55条黄金法则
c++