【数据结构】图

图的存储方式

邻接矩阵

优点

1.适合存稠密图
2.邻接矩阵判断两个顶点的连接关系,并取到权值时间复杂度为o(1)

缺点

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

实现

namespace matrix
{
	template<class V, class W,W MAX_W=INT_MAX, bool Direction=false>
	class Graph
	{
	public:
		//图的创建
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}
			_matrix.resize(n);
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				_matrix[i].resize(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& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t desti = GetVertexIndex(dst);

			_matrix[srci][desti] = w;
			if (Direction == false)
			{
				_matrix[desti][srci] = w;
			}
		}

		void Print()
		{
			//顶点
			int n = _vertexs.size();
			for (size_t i = 0; i < n; i++)
			{
				cout << i << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			//矩阵
			cout << "  ";
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				printf("%4d", i);
			}
			cout << endl;
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				cout << i << " ";//纵坐标
				for (size_t j = 0; j < _matrix[i].size(); j++)
				{
					if (_matrix[i][j] == MAX_W)
					{
						//cout << "*"<<" ";
						printf("%4c", '*');
					}
					else
					{
						//cout << _matrix[i][j] << " ";
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
		}

		//void BFS(const V& src)
		//{
		//	size_t srci = GetVertexIndex(src);
		//	int n = _matrix.size();
		//	queue<int> q;
		//	q.push(srci);
		//	vector<bool> vis(_vertexs.size(),false);
		//	vis[srci] = true;

		//	while (!q.empty())
		//	{
		//		int front = q.front();
		//		q.pop();
		//		cout << front << "->" << _vertexs[front] << " ";

		//		//邻接顶点入队列
		//		for (size_t i = 0; i < n; i++)
		//		{
		//			if (_matrix[front][i] != MAX_W&&!vis[i])
		//			{
		//				q.push(i);
		//				vis[i] = true;
		//			}
		//		}
		//	}
		//}
		

		void BFS2(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			int n = _matrix.size();
			queue<int> q;
			q.push(srci);
			vector<bool> vis(_vertexs.size(), false);
			vis[srci] = true;

			int count = 0;
			while (!q.empty())
			{
				int m = q.size();
				for (size_t i = 0; i <m; i++)
				{
					int front = q.front();
					q.pop();
					
					cout << count << "层朋友:" << _vertexs[front] << " ";

					//邻接顶点入队列
					for (size_t i = 0; i < n; i++)
					{
						if (_matrix[front][i] != MAX_W && !vis[i])
						{
							q.push(i);
							vis[i] = true;
						}
					}
				}
				count++;
				cout << endl;
			}
		}

		void _DFS(size_t srci, vector<bool>& vis)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			vis[srci] = true;

			//找一个srci没有访问过的点,去深度遍历
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				if (_matrix[srci][i] != MAX_W && vis[i] == false)
				{
					_DFS(i, vis);
				}
			}
		}


		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			vector<bool> vis(_vertexs.size(), false);

			_DFS(srci, vis);

			//如果不连通,还有检查vis哪里有false,循环遍历一遍
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				if (!vis[i])
					_DFS(i,vis);
			}
		}



	private:
		vector<V> _vertexs;		//顶点
		map<V, int> _indexMap;	//顶点映射下标
		vector<vector<W>> _matrix;//邻接矩阵
	};

	void Test_Graph1()
	{
		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();

		g.BFS2('0');
	}

	void Test_Graph2()
	{
		string a[] = { "张三", "李四", "王五", "赵六","周七","勾八"};
		Graph<string, int> g1(a, 6);
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "李四", 80);
		g1.AddEdge("李四", "勾八", 50);

		//g1.BFS2("张三");
		g1.DFS("王五");
	}

}


邻接表

用vector保存所有顶点,使用链表挂在每一个顶点下面,形成一个类似哈希桶的结构。

优点

1.适合存稀疏图
2.适合查找一个顶点连接出去的边

缺点

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

实现

namespace link_table
{
	template<class W>
	struct Edge
	{
		int _dsti;
		//int _srci;
		W _w;
		Edge<W>* _next=nullptr;

		Edge(const int dsti,const W& 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)
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}

			_tables.resize(n,nullptr);
		}

		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& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_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] = eg;
			}
		}


		void Print()const
		{
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				cout << i << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			for (size_t i = 0; i < _tables.size(); i++)
			{
				cout << _vertexs[i] << " " << i << "->";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout << _vertexs[cur->_dsti] << "[" << cur->_dsti << "]"<<cur->_w<<"->";
					cur = cur->_next;
				}
				cout << " nullptr" << endl;
			}
		}



	private:
		vector<V> _vertexs;//顶点集合
		map<V, int> _indexMap;//点和下标映射关系
		vector<Edge*> _tables;//边的集合
	};

	void test_link_tables()
	{
		string a[] = { "张三", "李四", "王五", "赵六"};
		Graph<string, int> g1(a, 4);
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);

		g1.Print();
	}
}

基本概念

完全图:在有n个顶点的无向图中,若有n *(n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n *(n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图。

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

连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边

最小生成树

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路
    构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。
    贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。

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>(Edge e2)const
			{
				return _w > e2._w;
			}
		};

		W Kruskal(Self& minTree)
		{
			//初始化minTree
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(_vertexs.size());
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				minTree._matrix[i].resize(_vertexs.size(), MAX_W);
			}


			priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
			int n = _matrix.size();

			for (size_t i = 0; i < n; i++)
			{
				for (size_t j = 0; j < n; j++)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						minq.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			//选出n-1条边
			UnionFindSet ufs(n);
			W totalW = W();
			int size = 0;
			while (!minq.empty())
			{
				Edge min = minq.top();
				minq.pop();

				if (!ufs.IsInSet(min._srci, min._dsti))
				{
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] <<
						":" << _matrix[min._srci][min._dsti] << endl;
					minTree._AddEdge(min._srci, min._dsti, min._w);
					ufs.Union(min._srci, min._dsti);
					totalW += min._w;
					size++;
				}
			}

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

这里首先创建一个Edge结构体用来存起始顶点和权值,采用优先级队列辅助每次取出最小的元素(用一个仿函数控制为权值比较的小堆),然后进行n-1次循环,每次把选取的Edge的顶点下标放进并查集中,最终若为最小生成树则返回权值和

Prim算法

从任意一个根节点开始,从这个起点出发选择一条轻量级的边,然后把这两个点归到集合A中,剩下的点在集合B中,再从集合B中选择一条小边再加入到集合A,循环往复,直到选出n-1条不成环的边。

代码实现

W Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			//初始化minTree
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			int n = _vertexs.size();
			minTree._matrix.resize(_vertexs.size());
			for (size_t i = 0; i < _vertexs.size(); i++)
			{
				minTree._matrix[i].resize(_vertexs.size(), MAX_W);
			}

			unordered_set<int> X;
			unordered_set<int> Y;

			X.insert(srci);
			for (size_t i = 0; i < n; i++)
			{
				if (i != srci)
				{
					Y.insert(i);
				}
			}

			//从X到Y集合中连最小边
			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 min = minq.top();
				minq.pop();

				if (X.count(min._dsti))
				{
					cout << "构成环" << _vertexs[min._srci] << ":" << _vertexs[min._dsti] << endl;
					continue;
				}

				minTree._AddEdge(min._srci, min._dsti,min._w);
				X.insert(min._dsti);
				Y.erase(min._dsti);
				totalW += min._w;
				size++;
				if (size == n - 1)
					break;

				for (size_t i = 0; i < n; i++)
				{
					if (_matrix[min._dsti][i] != MAX_W&&X.count(i)==0)
					{
						minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
					}
				}
			}

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


		}

版权声明:本文为博主作者:MLGDOU原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/m0_73865858/article/details/136686995

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2024年4月16日
下一篇 2024年4月16日

相关推荐