<高阶数据结构>图

本文介绍了图的基本概念,包括有向图和无向图,以及邻接矩阵和邻接表的存储方式。详细讲解了广度优先搜索(BFS)、深度优先搜索(DFS)、Kruskal最小生成树算法、Prim最小生成树算法和Dijkstra最短路径算法的应用。
摘要由CSDN通过智能技术生成

必要概念

图根据有无方向分为,有向图和无向图
组成:G = (V, E)

  • 顶点集合 V
  • 边的集合 E
    G(Graph),V(Vertex),E(Edge)
    图可以说是一个灰常抽象且学习比较有挑战性的一个数据结构,一个图是由顶点集合和边集合组成的

大致用途

  • 表示交通网络图,例如,顶点是城市,边是城市之间的距离
  • 表示社交关系图

存图

一般存储我们有两种方法

邻接矩阵

  • 采用vector保存顶点
  • 采用矩阵(二维数组)保存边
    在这里插入图片描述

优点:

  1. 非常适合存储稠密图
  2. O(1)判断两个顶点的连接关系,并取得权值

缺点:

  1. 相对而言不适合用来查找一个顶点连接的所有边O(N)
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
		{
			//assert(false);
			throw std::invalid_argument("顶点不存在");

			return -1;
		}
	}

	void _AddEdge(size_t srci, size_t dsti, const W& w)
	{
		// 无向图
		if (Direction == false)
		{
			_matrix[srci][dsti] = w;
			_matrix[dsti][srci] = w;
		}
		else
		{
			_matrix[srci][dsti] = w;
		}
	}

	void AddEdge(const V& src, const V& dst, const W& w)
	{
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);

			_AddEdge(srci, dsti, w);
		}
	}

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

		cout << "  ";
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			//cout << i << " ";
			printf("%-4d", i);
		}
		cout << endl;

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

private:
	std::vector<V> _vertexs;		// 顶点集合
	std::map<V, int> _indexMap;		// 顶点对应的下标关系
	std::vector<std::vector<W>> _matrix; // 邻接矩阵
};

邻接表

  • 使用vector保存所有的顶点
  • 使用链表保存与每个顶点连通的顶点

优点:

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

缺点:

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

namespace link_table
{
	template<class W>
	struct Edge
	{
		int _dsti; // 目标点的下标
		W _w; // 权值
		Edge* _next;

		Edge(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:
		// 图的创建
		// 1.IO输入 -- 不方便测试 OJ适合
		// 2.图结构关系写到文件,读取文件
		// 3.手动添加边 -- 方便测试修改
		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
			{
				//assert(false);
				throw std::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);

				// 1->2
				Edge* eg = new Edge(dsti, w);
				eg->_next = _tables[srci];
				_tables[srci] = eg;
				// 如果是无向图 2->1
				if (Direction == false)
				{
					Edge* eg = new Edge(srci, w);
					eg->_next = _tables[dsti];
					_tables[dsti] = eg;
				}
			}
		}

		void Print()
		{
			// 打印顶点
			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:
		std::vector<V> _vertexs;		// 顶点集合
		std::map<V, int> _indexMap;		// 顶点对应的下标关系
		std::vector<Edge*> _tables; // 邻接表
	};
	        
	void TestGraph1()
	{
		Graph<char, int, 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();
	}
}

遍历

BFS(广度优先)

以某一顶点为起点,一层一层的向外遍历
在这里插入图片描述
我们只需借助一个队列来辅助实现,这里我们先将A入队列,在A出队列的时候,将A连通的最近一层B,C,D入队列,B出队列时将E入队列,如此运行直到队列为空时,遍历结束,出队列顺序即时我们的遍历次序
为了防止B出队列时,再次将A和C入队列,可以开一个标记容器,标记入了队列的顶点

void BFS(const V& src)
{
	size_t srci = GetVertexIndex(src);
	// 队列和标记数组
	std::queue<int> q;
	std::vector<bool> visited(_vertexs.size(), false);

	q.push(srci);
	visited[srci] = true;
	int levelSize = 1;

	while (!q.empty())
	{
		// 一层一层出
		for (size_t i = 0; i < levelSize; ++i)
		{
			int 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)
				{
					if (visited[i] != true)
					{
						q.push(i);
						visited[i] = true;
					}
				}
			}
			cout << endl;
		}
		levelSize = q.size();
	}

	cout << endl;
}

DFS(深度优先)

以某一顶点为起点,深度优先遍历
在这里插入图片描述


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

	// 找一个srci相邻的没有访问过的点,去往深度遍历	
	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);
	std::vector<bool> visited(_vertexs.size(), false);

	_DFS(srci, visited);
}

最小生成树

  • 构成生成树这些边加起来权值是最小的
    这里采用两种算法,两种算法都采取了贪心的策略

Kruskal算法

方法
每次找权值最小边,注意这个最小边不能构成环,将所有顶点连接起来结束算法
可以借助一个并查集结构,用于判断已选的边是否构成环
这个算法是看的局部最优边,只关注局部最优
下图是算法笔记这本书里面的算法流程图
在这里插入图片描述
定义个Edge边结构,辅助算法

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;
	}
};

W Kruskal(Self& minTree)
{
	// 初始化minTree
	size_t n = _matrix.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);
	}

	std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge >> minque;
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		for (size_t j = 0; j < _matrix[i].size(); ++j)
		{
			if (j > i && _matrix[i][j] != MAX_W)
			{
				minque.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	//选出n-1条边
	W totalw = 0;
	int size = 0;
	UnionFindSet ufs(_matrix.size());
	while (!minque.empty())
	{
		Edge eg = minque.top();
		minque.pop();

		if (!ufs.InSet(eg._srci, eg._dsti))
		{
			cout << _vertexs[eg._srci] << "->" << _vertexs[eg._dsti] << endl;
			minTree._AddEdge(eg._srci, eg._dsti, eg._w);
			ufs.Union(eg._srci, eg._dsti);
			++size;
			totalw += eg._w;
		}
	}

	if (size == n - 1)
		return totalw;
	else
		return -1;
}

Prim算法

方法
从某一顶点开始,选择该顶点连接的边中权值最小的边,再到下一顶点选连接的边中最小权值的边,同样需要记录一下已经选过了的顶点
借助一个队列实现,和广度优先遍历方法有点类似,只不过这里只需要选择最小权值边的顶点

int Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			// 初始化minTree
			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> visited(n, false);
			std::priority_queue < Edge, std::vector<Edge>, std::greater<Edge >> pq;

			for (size_t i = 0; i < n; ++i)
			{
				if (_matrix[srci][i] != MAX_W)
				{
					pq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}
			
			size_t size = 0;
			visited[srci] = true;
			int total = 0;
			while (!pq.empty())
			{
				Edge front = pq.top();
				pq.pop();

				if (visited[front._dsti] == true)
				{
					continue;
				}

				cout << _vertexs[front._srci] << "->" << _vertexs[front._dsti] << endl;
				minTree._AddEdge(front._srci, front._dsti, front._w);

				if (size == n -1)
				{
					break;
				}
				
				// 入队列
				for (size_t i = 0; i < n; ++i)
				{
					if (visited[i] != true && _matrix[front._dsti][i] != MAX_W)
					{
						pq.push(Edge(front._dsti, i, _matrix[front._dsti][i]));
					}
				}

				++size;
				total += front._w;
				visited[front._dsti] = true;
			}

			return total;
		}

寻最短路径

找某个顶点到图中另一顶点走的最短路径

Dijkstra算法

使用的也是贪心的策略

  • 将选择了的顶点和没有选择的顶点分为两个集合
  • dist记录从s顶点到Q顶点的最短路径权值和
  • pPath记录满足最短路径时每个顶点的上一个顶点下标
  • 定义一个S数组记录已经确定的最短顶点,一旦选定不可更改
    下图
  1. 起始,s到s的最短路径权值和为0,dist[0]=0,上一个顶点为s下标为0,pPath[0]=0
  2. s起点,s到y的最短路径权值和为5. dist[1]=5,pPath[1]=0,s到t的最短路径权值和为10,dist[3]=10,pPath[3]=0
  3. 选权值最小的y作为起点,s通过y到t的最短路径权值和为8. 8 < 10, 更新dist[3]=8,pPath[3]=1,s通过y到x最短路径权值和为14,dist[4]=14,pPath[4]=1, ,s通过y到z最短路径权值和为7,dist[2]=7,pPath[4]=1,
    在这里插入图片描述
void PrintShortPath(const V& src, const vector<int>& dist, const vector<int>& parentPath)
{
	size_t N = _vertexs.size();
	size_t srci = GetVertexIndex(src);
	for (size_t i = 0; i < N; ++i)
	{
		if (i == srci)
			continue;

		vector<int> path;
		int parenti = i;
		while (parenti != srci)
		{
			path.push_back(parenti);
			parenti = parentPath[parenti];
		}
		path.push_back(srci);
		reverse(path.begin(), path.end());
		for (auto pos : path)
		{
			cout << _vertexs[pos] << "->";
		}
		cout << dist[i] << endl;
	}
}

void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();

	dist.resize(n, MAX_W);
	pPath.resize(n, -1);

	dist[srci] = W();
	pPath[srci] = 0;

	vector<bool> s(n, false);

	for (size_t i = 0; i < n; ++i)
	{
		W min = MAX_W;
		size_t u = srci;
		for (size_t j = 0; j < n; ++j)
		{
			if (s[j] != true && dist[j] < min)
			{
				min = dist[j];
				u = j;
			}
		}

		// 松弛算法
		for (size_t k = 0; k < n; ++k)
		{
			if (s[u] == false && _matrix[u][k] != MAX_W
				&& dist[u] + _matrix[u][k] < dist[k])
			{
				dist[k] = dist[u] + _matrix[u][k];
				pPath[k] = u;
			}
		}
		s[u] = true;

	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绅士·永

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值