【数据结构】图算法(代码)

一、图的遍历

1.1 图的广度优先遍历

这里我用的上一章节中图的邻接矩阵的结构,进行遍历的。

这里其实跟二叉树中,层序遍历思想差不多,都是前一个节点带后面的节点(A带B,C,D之后依次类推)当然这里有一个难点就是当我们遍历到B的时候我们插入队列的时候有AEC,但是我们只想要E,因为A已经pop掉了,C在队列中。我们该如何操作?用个数组标记一下全部元素,当前要插入队列中的元素是否用过,也就是说当我们元素插入到队列的时候标记一下。

代码理解一下

复制代码
		void BFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			queue<size_t> q;
			// 标记数组
			vector<bool> visited(_vertexs.size(), false);

			q.push(srci);
			visited[srci] = true;
			size_t levelSize = 1;
			while (!q.empty())
			{
				while (levelSize--)
				{
					size_t front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front];

					// 把front顶点的邻接顶点入队列
					for (size_t i = 0; i < _vertexs.size(); ++i)
					{
						// 过滤掉已经入队的
						if (_matrix[front][i] != MAX_W && visited[i] == false)
						{
							q.push(i);
							visited[i] = true;
						}
					}
				}
				cout << endl;
				// 更新levelSize
				levelSize = q.size();
			}
		}

测试结果

1.2 图的深度优先遍历

这种就是递归,不用管上图标注的对不对,get到思想即可。

复制代码
		void _DFS(size_t srci, vector<bool>& visited)
		{
			// 遍历当前结点并标记
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;

			// 寻找当前节点没有被访问过的邻接顶点,继续递归往深度遍历
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				if (_matrix[srci][i] != MAX_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			// 标记数组
			vector<bool> visited(_vertexs.size(), false);

			_DFS(srci, visited);
		}

二、最小生成树

构造最小生成树的方法: Kruskal 算法Prim 算法 。这两个算法都采用了 逐步求解的贪心策略

在学习 Kruskal****算法Prim****算法的时候要先了解一下什么是并查集。

并查集

在一些应用问题中,需要 n 个不同的元素划分成一些不相交的集合开始时,每个元素自成一个
单元素集合,然后按一定的规律将归于同一组元素的集合合并 。在此过程中 要反复用到查询某一
个元素归属于那个集合的运算 。适合于描述这类问题的抽象数据类型称为 并查集 (union-find
set)
比如:某公司今年校招全国总共招生 10 人,西安招 4 人,成都招 3 人,武汉招 3 人, 10 个人来自不
同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号: {0, 1, 2, 3,
4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数。( 负号下文解释 )

毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起上路,于是:
西安学生小分队 s1={0,6,7,8} ,成都学生小分队 s2={1,4,9} ,武汉学生小分队 s3={2,3,5} 就相互认识
了, 10 个人形成了三个小团体。假设右三个群主 0,1,2 担任队长,负责大家的出行。

一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。


从上图可以看出:编号 6,7,8 同学属于 0 号小分队,该小分队中有 4 人 ( 包含队长 0) ;编号为 4 和 9 的同
学属于 1 号小分队,该小分队有 3 人 ( 包含队长 1) ,编号为 3 和 5 的同学属于 2 号小分队,该小分队有 3
个人 ( 包含队长 1) 。
仔细观察数组中内融化,可以得出以下结论:

  1. 数组的下标对应集合中元素的编号
  2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数
  3. 数组中如果为非负数,代表该元素双亲在数组中的下标

并查集一般可以解决以下问题:

  1. 查找元素属于哪个集合
    沿着数组表示树形关系往上一直找到根( 即:树中中元素为负数的位置 )
  2. 查看两个元素是否属于同一个集合
    沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在
  3. 将两个集合归并成一个集合
    将两个集合中的元素合并
    将一个集合名称改成另一个集合的名称
  4. 集合的个数
    遍历数组,数组中元素为负数的个数即为集合的个数。

代码实现一下

复制代码
class UnionFindSet
{
public:
	UnionFindSet(int n)
		:_ufs(n, -1)
	{}
	int FindRoot(int x)
	{
		int root = x;
		while (_ufs[root] >= 0)
			root = _ufs[root];

		 //路径压缩
		while (_ufs[x] >= 0)
		{
			int parent = _ufs[x];
			_ufs[x] = root;
			x = parent;
		}
		return root;
	}

	bool Union(int x, int y)
	{
		int root1 = FindRoot(x);
		int root2 = FindRoot(y);

		if (root1 == root2)
		{
			return false;
		}
		// -20    -100 
		if (_ufs[root1] > _ufs[root2])
		{
			swap(root1, root2);
		}

		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;

		return true;
	}
	bool InSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}
	int SetSize()
	{
		int count = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				++count;
		}
		return count;
	}
private:
	vector<int> _ufs;
};

2.1、Kruskal算法

克鲁斯卡尔算法

步骤:找最小的边 ,不能成环

定义一个边的类

复制代码
		struct Edge
		{
			size_t _srci;
			size_t _dsti;
			W _w;

			Edge(size_t srci, size_t dsti, const W& w)
				:_srci(srci)
				,_dsti(dsti)
				,_w(w)
			{}

			bool operator > (const Edge& e) const
			{
				return _w > e._w;
			}
		};

这里就是先找最小边,并且节点相连的边不为环 ,如何判断相连的边不为环用并查集,比如我们看图f和图g,为什么最后选了7没选6,因为选6这个边构成环了,怎么判断构成的环?这个边的i节点与g节点他们能找到共同的根节点。

代码实现一下

复制代码
		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 (i < j && _matrix[i][j] != MAX_W)
					{
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			// 依次从priority_queue中选最小n-1条边连接minTree即可(判断构成环的不能选)
			int size = 0;
			UnionFindSet ufs(n);
			W totalW = W();
			while (!minque.empty())
			{
				Edge minEg = minque.top();
				minque.pop();

				if (!ufs.InSet(minEg._srci, minEg._dsti))
				{
					cout << _vertexs[minEg._srci] << "->" << _vertexs[minEg._dsti] << ":" << minEg._w << endl;
					minTree._AddEdge(minEg._srci, minEg._dsti, minEg._w);
					ufs.Union(minEg._srci, minEg._dsti);
					++size;
					totalW += minEg._w;
				}
				else
				{
					cout << " 构成环 : ";
					cout <<  _vertexs[minEg._srci] << "->" << _vertexs[minEg._dsti] << ":" << minEg._w << endl;
				}

			}
			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W();
			}
		}

验证一下

2.2、Prim****算法

普利姆算法

步骤:通过选中的节点,找未选中的节点的边,且不能成环

这里我借助了,两个数组X,Y标记该节点是否插入。X表示选中的边,Y表示未选中的边

刚开始我们选了a为节点,这里我们用堆,把a的两个边插入到堆中,并在X中标注a已经选中,在Y中标注为已经选过了。我们这里通过堆排序筛选出了ab这个边最小又因为b不在X中,这里我们在把与b相连的边添加到堆中,这里我们要注意一下重复边ba是不能插入的,因为这个边我们已经选过了,如何判断ba是不能插入的呢?这里我们用到了Y数组我们只要与b相连且未被选中的节点我们就可以插入到堆中。按照这个思路以此类推

代码实现一下:

复制代码
		W Prim(Self& minTree, const W& 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<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)
			{
				if (_matrix[srci][i] != MAX_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}
			// 选最小
			size_t size = 0;
			W totalW = W();
			while (!minq.empty())
			{
				Edge minEg = minq.top();
				minq.pop();
				// 最小边的目标点也在X集合,则构成环
				if (X[minEg._dsti])
				{
					// 构成环
					cout << "构成环: " << minEg._srci << "->" << minEg._dsti << endl;
				}
				else
				{
					cout << "加边: " << minEg._srci << "->" << minEg._dsti << " [" <<minEg._w<<"]" << endl;
					minTree._AddEdge(minEg._srci, minEg._dsti, minEg._w);
					X[minEg._dsti] = true;
					Y[minEg._srci] = false;
					++size;
					totalW += minEg._w;
					if (size == n - 1)
						break;

					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[minEg._dsti][i] != MAX_W && Y[i])
						{
							minq.push(Edge(minEg._dsti, i, _matrix[minEg._dsti][i]));
						}
					}
				}
			}
            // 判断是否只有一个树
			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W();
			}
		}

验证一下:

三、最短路径

最短路径问题:从在带权有向图 G 中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。

3.1、Dijkstra****算法

迪杰斯特拉算法
Dijkstra 算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路
径的最短路径。

步骤:以s为起点,更新相连的节点,选出相连最小的边y,然后更新一下y到其他节点的边,更新相连的节点为最小值。以此类推....

这里我定义了一个s,dist,pPath数组。

s数组表示没有选中过的节点,dist表示起始节点到该下标节点的最短路径,pPath数组这里存储他们的父节点。

这里的算法思想:我要找到s数组中没有选中的节点且dist最小,之后进行松弛更新,更新一下该节点到邻接点的距离,之后把选中的节点标注为true表示选过了,之后再在未选中的节点中找dist中最小的值。

复制代码
// 顶点个数是N  -> 时间复杂度:O(N^2)空间复杂度:O(N)
void Dijkstra(const V& src, vector<int>& pPath, vector<W>& dist)
{	
	// 初始化一下记录路径和权值(距离)的数组
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();
	pPath.resize(n, -1);
	dist.resize(n, MAX_W);

	// 集合S为已确定最短路径的顶点集合
	vector<bool> s(n, false);

	pPath[srci] = srci;
	dist[srci] = 0;

	for (size_t i = 0; i < n; i++)
	{
		// 选最短路径顶点且不在S更新其他路径
		int u = 0; // u为找到最小路径的下标
		W minDist = MAX_W;
		for (size_t j = 0; j < n; j++)
		{
			if (s[i] == false && dist[i] < minDist)
			{
				u = i;
				minDist = dist[i];
			}
		}
		s[u] = true;
		// 松弛更新,更新起点到它相邻点的距离
		for (size_t v = 0; v < n; v++)
		{
			if (s[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
			{
				dist[v] = dist[u] + _matrix[u][v];
				pPath[v] = u;
			}
		}
	}
}

验证一下:

3.2、Bellman-Ford****算法

贝尔曼-福特算法
bellman---ford 算法可以解决负权图的单源最短路径问题。
先解释一下为什么迪杰斯特拉算法 解决不了负权图的单源最短路径问题,正因为没有负权值导致所有的节点只走一遍,如果遇到负权值把原来,原点到节点的距离进行修改而这个节点并且已经被标注为true了表示选中过的节点。那么与这个节点相连的节点的值全部都要进行修改,因为原点到这个节点的距离进行了修改。


步骤:以s为起始节点,对与s相连的节点进行松弛,把t,y节点进行更新,之后在依次更新与t,y相连的节点....以此类推....

这里我们还是采用了三个数组s,dist,pPath

这里我们采用暴力搜索的算法,先初始化节点s,然后更新与之相连的边t,y,这次我们不像迪杰斯特拉算法找最小边,而是更新所有t,y的边,然后找到t,y相连的所有节点,再依次更新与之相连的所有边。

复制代码
		// 解决负权值
		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			size_t n = _vertexs.size();
			size_t srci = GetVertexIndex(src);
			// vector<W> dist,记录srci-其他顶点最短路径权值数组
			dist.resize(n, MAX_W);
			// vector<int> pPath 记录srci-其他顶点最短路径父顶点数组
			pPath.resize(n, -1);
			// 先更新srci->srci为缺省值
			dist[srci] = W();
			cout << "映射关系: " << endl;
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;
			// 总体最多更新n轮
			for (size_t k = 0; k < n; ++k)
			{
				// i->j 更新松弛
				bool update = false;
				cout << "更新第:" << k << "轮" << endl;
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
						{
							update = true;
							dist[j] = dist[i] + _matrix[i][j];
							pPath[j] = i;
						}
					}
				}
				if (update == false)
				{
					break;
				}
			}

			// 还能更新就是带负权回路
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					// srci -> i + i ->j
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}
			cout << "dist:" << endl;
			for (size_t i = 0; i < n; i++)
			{
				cout << "[" << 0 << "]->[" << i << "]->" << dist[i] << endl;
			}
			return true;

		}

效果展示一下:

3.3、Floyd-Warshall 算法

弗洛伊德算法是一种解决多源最短路径问题(任意两点间的最短路径)的算法。

Di,j,k表示从i到j的最短路径,该路径经过的中间结点是剩余的结点组成的集合中的结点,假设经过k个结点,编号为1...k,然后这里就分为了两种情况:

1.如果路径经过了结点k,那么ij的距离就等于ik的距离加上kj的距离,然后剩余就经过k-1个点

2.如果不经过结点k,那ij的距离就等于i到j经过k-1个点(不包括k)的距离

复制代码
		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			size_t n = _vertexs.size();
			vvDist.resize(n);
			vvpPath.resize(n);

			// 初始化权值和路径矩阵
			for (size_t i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}
			// 直接相连的边更新一下
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvpPath[i][j] = i;
					}

					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}


			// 最短路径的更新 i-> {其他顶点} -> j
			for (size_t k = 0; k < n; ++k)
			{
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						// k 作为的中间点尝试更新i->j的路径
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W
							&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
							// 找跟j相连的上一个邻接顶点
							// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
							// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x
							vvpPath[i][j] = vvpPath[k][j];
						}
					}	
				}

			}
        }
相关推荐
慢半拍iii2 小时前
数据结构——D/串
c语言·开发语言·数据结构·c++
怀旧,2 小时前
【数据结构】5. 双向链表
数据结构·windows·链表
王景程2 小时前
什么是哈希函数
算法·哈希算法
会不再投降2192 小时前
《算法复杂度:数据结构世界里的“速度与激情”》
数据结构·算法
vvilkim2 小时前
深入解析 Pandas 核心数据结构:Series 与 DataFrame
数据结构·pandas
Frankabcdefgh3 小时前
Python基础数据类型与运算符全面解析
开发语言·数据结构·python·面试
kaiaaaa3 小时前
算法训练第十五天
开发语言·python·算法
Coovally AI模型快速验证3 小时前
SLAM3R:基于单目视频的实时密集3D场景重建
神经网络·算法·3d·目标跟踪·音视频
Once_day4 小时前
代码训练LeetCode(29)最后一个单词的长度
算法·leetcode·c
凌肖战4 小时前
力扣上C语言编程题:最大子数组和(涉及数组)
c语言·算法·leetcode