【数据结构】图论与并查集

一、并查集

1.原理

  1. 简单的讲并查集,就是查询两个个元素,是否在一个集合当中,这里的集合的形式进行表示。
  2. 并查集的本质就是森林, 即多棵树。

 我们再来简单的举个例子:

  • 假设此时的你是大一新生,刚进入大学,肯定是先找到宿舍在哪里,然后跟寝室里面的舍友互相认识一下,先形成一个小团体。
  • 假设,宿舍总共6个人,也就是6个人的集合。几乎所有的大学生都是这样先跟周围的人进行联系起来的。
  • 然后辅导员召集班会,这时的你欣然前往,并在讲台上自信的介绍自己,然后吸引或者主动又认识了一群人。这时你或许又跟其它的人进行了关联,或成为了好友,或成为了恋人……

下面我们用如上例子进行展开讨论:

  • 宿舍六人,即六个人,如何判断两个人在同一个集合? 如何进行实现?
  1. 先来解决第一个问题,六个人,选出一个宿舍长,只要两个人的宿舍长是一样的,即可判断两个人在一个集合。
  2. 再来解决第二个问题,既然宿舍长有了,我们都与这个宿舍长产生关联即可,即用树的形式进行表示,至于如何表示,我们可以用双亲表示法进行表示,即每个人记住其宿舍长的名字即可。更为形象的我们可以用下图进行表示:
  3. 更进一步,如何用计算机存储这种结构呢?我们只需对每个人名生成一个下标连续,用计算机进行存储即可。用下图进行直观的理解:
    在这里插入图片描述
  4. 对这张图我们再说明一点,除0下标以外的其他位置存放的是指向代表孙八的下标,这个0处下标存的是集合的所有元素的个数,且存放的是负数形式,这样存有一个好处,我们可以由这个并查集中有多少负数,从而判断这个并查集中有多少个集合。
  • 两个人产生关联,本质上是两个宿舍(集合)之间产生了关联,那两个宿舍如何进行关联起来呢?
  • 下面我们以图的形式更为清晰的进行表述:
    在这里插入图片描述
  • 也就是说因为宿舍的成员是以宿舍长联系起来的,那宿舍与宿舍之间,产生关联(合并),就宿舍长之间认识一下,两个集合就间接的关联起来了。
  • 下图是具体的存储方式:
    在这里插入图片描述

2.基本实现

 根据上面的描述,我们可以作出大致总结:

  1. 数组进行存储表示树形结构。
  2. 数组的下标对应着具体的信息(人名,编号等)。
  3. 我们可以通过一个元素的下标的值不断往上查找,直到找到找到小于0的,即为根节点所在的位置。
  4. 数组中负数的个数代表着集合的个数。
  5. 判断两个元素是否在同一个集合,只需找到根的下标判断是否相等即可。
  6. 将两个不同集合进行合并,其实就是找到根,然后进行更改一个根的指向与改变另一个根的元素个数即可。

由以上信息我们先可以搭建出实现并查集的大致框架:

2.1.基本框架

#include<iostream>
#include<vector>
#include<map>
using namespace std;
template<class T>
class UnionFindSet
{
public:UnionFindSet(const T* arr, size_t size);//构造函数int GetValueIndex(const T& val);//获取val所代表的下标。void GetRoot(const T& val);//获取根节点的下标void Union(const T& x1, const T& x2);//将两个元素的集合进行合并。bool IsSameSet(const T& x1, const T& x2);//判断两个元素是否在同一个集合中int GetSetSize(); //获取集合的元素
private:map<T, int> _indexHash;//map或者unordered_map都可以。用于快速将T转换为对应的下标。vector<T> _createIndex;//用此数组对T类型元素生成下标。vetor<int> _aggregate; //用于存放集合元素,即森林。
};

2.2.构造函数

	UnionFindSet(const T* arr, size_t size){_aggreagte.resize(size, -1);//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。_createIndex.resize(size);for (size_t i = 0; i < size; i++){_createIndex[i] = arr[i];_indexHash[arr[i]] = i;//生成下标。}}

2.3.转换元素为下标

	int GetValueIndex(const T& val){auto it = _indexHash.find(val);//最好判断一下val是否存在对应的下标。if (it == _indexHash.end()){throw invalid_argument("不存在所对应的下标");return -1;}return it->second;}

2.4.获取元素根节点下标

	int GetRoot(const T& val){int index = GetValueIndex(val);//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[index] >= 0){index = _aggregate[index];}return index;}

2.5.判断元素集合是否相同

	bool IsSameSet(const T& x1, const T& x2)/{int index1 = GetRoot(x1);int index2 = GetRoot(x2);return index1 == index2;}

2.6.合并元素集合

	void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。{if (!IsSameSet(x1, x2)){//不在同一个集合再进行合并。int index1 = GetRoot(x1);int index2 = GetRoot(x2);//进行一步优化,即元素少的合并到元素多的集合当中//此处我们假设index1为元素多的集合,index2为元素少的集合。if (abs(index1) < abs(index2)){swap(index1, index2);}//即将index2(少)合并到index1(多)上//将index2的元素加到index2上_aggregate[index1] += _aggregate[index2];//将index2的父路径指向index1_aggregate[index2] = index1;}}

2.7.获取集合个数

	int GetSetSize()//获取并查集的集合个数{int sum = 0;for (auto e : _aggregate){//计算小于0的元素个数即可。if (e < 0){sum++;}}return sum;}

3.路径压缩

 所谓路径压缩,其实解决存在这样的集合:
在这里插入图片描述
所引发的问题:如果数据足够的多,我们之前写的GetRoot函数的效率会急剧的降低,因此才需要路径压缩帮助我们进行优化。

实现方式也很简单:
在这里插入图片描述

  • 我们只需要找到根节点之后,再找一遍,此时将cur路径上的结点链接到root即可,这样方便了后续的查找。

  • 优化之后的GetRoot

	int GetRoot(const T& val)//获取根节点的下标{int index = GetValueIndex(val);int root = index;//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[root] >= 0){root = _aggregate[root];}//路径压缩进行优化。while (index != root){//先保存之前父路径的下标int parent = _aggregate[index];//再将当前结点的父路径改为root_aggregate[index] = root;//继续往上迭代index = parent;}return root;}

4.源码与测试

  • UnionFindSet.hpp
#include<iostream>
#include<vector>
#include<map>
using namespace std;
template<class T>
class UnionFindSet
{
public:UnionFindSet(const T* arr, size_t size){_aggregate.resize(size, -1);//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。_createIndex.resize(size);for (size_t i = 0; i < size; i++){_createIndex[i] = arr[i];_indexHash[arr[i]] = i;//生成下标。}}int GetValueIndex(const T& val)//获取val所代表的下标。{auto it = _indexHash.find(val);if (it == _indexHash.end()){throw invalid_argument("不存在所对应的下标");return -1;}return it->second;}int GetRoot(const T& val)//获取根节点的下标{int index = GetValueIndex(val);int root = index;//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[root] >= 0){root = _aggregate[root];}//路径压缩进行优化。while (index != root){//先保存之前父路径的下标int parent = _aggregate[index];//再将当前结点的父路径改为root_aggregate[index] = root;//继续往上迭代index = parent;}return root;}void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。{if (!IsSameSet(x1, x2)){//不在同一个集合再进行合并。int index1 = GetRoot(x1);int index2 = GetRoot(x2);//进行一步优化,即元素少的合并到元素多的集合当中//此处我们假设index1为元素多的集合,index2为元素少的集合。if (abs(index1) < abs(index2)){swap(index1, index2);}//即将index2(少)合并到index1(多)上//将index2的元素加到index2上_aggregate[index1] += _aggregate[index2];//将index2的父路径指向index1_aggregate[index2] = index1;}}//判断两个元素是否在同一个集合中bool IsSameSet(const T& x1, const T& x2){int index1 = GetRoot(x1);int index2 = GetRoot(x2);return index1 == index2;}int GetSetSize()//获取并查集的集合个数{int sum = 0;for (auto e : _aggregate){if (e < 0){sum++;}}return sum;}
private:map<T, int> _indexHash;//map或者unordered_map都可以,用于快速将T转换为对应的下标。vector<T> _createIndex;//用此数组对T类型元素生成下标。vector<int> _aggregate; //用于存放集合元素,即森林。
};
  • Test.cpp
#include"UnionFindSet.hpp"
int main()
{string str[] = { "张三","李四","王五","赵六","周七" };UnionFindSet<string> ufs(str, sizeof(str) / sizeof(str[0]));ufs.Union("张三", "李四");ufs.Union("王五", "赵六");cout << "集合数为:" << ufs.GetSetSize() << endl;return 0;
}

运行结果:
在这里插入图片描述

并查集习题:

  1. 省份数量
  2. .等式方程的可满足性
  • 补充一下:
  1. 直接用下标进行抽象,是最常用的,因此这里的生成下标的vector与快速索引的map可以省去,形成一个简化版的并查集,更方便我们使用。
  2. 这里我们将并查集与图论放在一起,是因为并查集可以帮助起到判环的作用,因此我们这里放到一块进行讲解。

二、图论

1.基本概念

  • 图的概念有点凌乱,博主以思维导图的形式呈现出:

在这里插入图片描述

2.存储结构

  • 图有两个基本元素:
  1. 顶点, 我们可以将具体的顶点抽象成下标,从而用下标进行表示。
  2. 边,两个顶点即可确定一条边,因此我们可以用二维矩阵的方式进行表示;每个顶点都有与其相连的边,因此,我们可以单独每个顶点所连接的边抽象成桶的形式(类似于哈希桶)进行表示。
  • 因此我们通常有邻接矩阵和邻接表的形式进行存储。

2.1邻接矩阵

  • 实现代码:
	/*V(vertex) 表示实际存储边的类型,W(weight)表示边的权重,W_MAX 表示权重的不可能取值。Direction false表示是无向的,true表示是有向的。*/template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>class Graph{public:/*构造函数,传入的参数为V类型的指针指向的是V类型数组,以及数组的元素个数。*/Graph(const V* a, size_t n)//有多少个顶点{//初始化边,以及生成边的下标_vertexs.resize(n);for (size_t i = 0; i < n; i++){_vertexs[i] = a[i];_indexMap[a[i]] = i;}//将矩阵进行初始化_matrices.resize(n);for (size_t i = 0; i < n; i++){//没有权值,我们初始化为W_MAX,表示最开始顶点之间不互相连通。_matrices[i].resize(n, W_MAX);}}//将实际的顶点转换为对应的下标int GetVertexIndex(const V& v){auto it = _indexMap.find(v);if (it == _indexMap.end()){//找不到throw invalid_argument("顶点不存在");//抛出异常return -1;}return it->second;}//添加边void AddEdge(const V& src, const V& dst, const W& w){int srci = GetVertexIndex(src);int dsti = GetVertexIndex(dst);_AddEdge(srci, dsti, w);}//这里我们写一个子函数,方便内部接口进行使用。void _AddEdge(int srci, int dsti, const W& w){_matrices[srci][dsti] = w;if (Direction == false){//说明是无向图_matrices[dsti][srci] = w;}}//为了方便进行测试,这里博主将打印函数给出。void Print(){for (size_t i = 0; i < _vertexs.size(); i++){printf("[%d]->", i);cout << _vertexs[i] << endl;//下标对应的边}cout << "    ";for (size_t i = 0; i < _matrices.size(); i++)printf("%-4d", i);cout << endl;for (size_t i = 0; i < _matrices.size(); i++){printf("%-4d",i);for (size_t j = 0; j < _matrices[i].size(); j++){if (_matrices[i][j] != W_MAX)printf("%-4d", _matrices[i][j]);elseprintf("%-4c", '*');}cout << endl;}cout << endl;}vector<V> _vertexs;//顶点map<V, int> _indexMap;//顶点所对应的下标vector<vector<W>> _matrices; //矩阵的英文};
  • 说明:
  1. 如果边带有权值,并且两个节点之间是连通的,边的关系就用权值代替。
  2. 如果两个顶点不通,则使用无穷大代替,即W_MAX。
  • 测试用例:

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();
}
int main()
{TestGraph();return 0;
}

运行结果:
在这里插入图片描述

2.2邻接表

  • 实现代码:
namespace link
{/*因为要存顶点与边的关系,因此我们需要一个结构体来保存对应的相连的顶点与边的权值。*/template<class V,class W>struct Edge{V _dst;//目标顶点W _w;//权值Edge<V, W>* _next;//构造函数Edge(const V& dst, const W w):_dst(dst),_w(w),_next(nullptr){}};template<class V, class W, bool Direction = false>class Graph{public:typedef Edge<V, W> Edge;Graph(const V* a, size_t n)//有多少个顶点{//初始化边,以及生成对应的下标_vertexs.resize(n);for (size_t i = 0; i < n; i++){_vertexs[i] = a[i];_indexMap[a[i]] = i;}//将矩阵进行初始化,为空表示最开始顶点没有边与之相连。_link.resize(n,nullptr);}//添加边void AddEdge(const V& src, const V& dst, const W& w){int srci = GetVertexIndex(src);int dsti = GetVertexIndex(dst);Edge* node = new Edge(dst, w);node->_next = _link[srci];_link[srci] = node;if (Direction == false){//说明是无向图Edge* node = new Edge(src, w);node->_next = _link[dsti];_link[dsti] = node;}}//获取顶点的下标。int GetVertexIndex(const V& v){auto it = _indexMap.find(v);if (it == _indexMap.end()){//找不到throw invalid_argument("顶点不存在");//抛出异常return -1;}return it->second;}//打印的时候我们按照链表的形式打印即可。void Print(){for (size_t i = 0; i < _link.size(); i++){cout << "[" << i << ":" << _vertexs[i] << "]->";Edge* cur = _link[i];while (cur){cout << "[" << cur->_dst << ":" << _indexMap[cur->_dst] << ":" << cur->_w << "]->";cur = cur->_next;}cout << "nullptr" << endl;}cout << endl;}private:vector<V> _vertexs;//顶点map<V, int> _indexMap;//顶点所对应的下标vector<Edge*> _link; //邻接表};
}
  • 测试用例:
void TestGraph()
{string a[] = { "张三", "李四", "王五", "赵六" };Graph<string, int,true> g1(a, 4);g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.Print();
}

运行结果:

在这里插入图片描述


  • 总结:
  1. 邻接矩阵适合快速查看两个顶点的关系与路径权值。而对于顶点连接的边有多少,是什么,则需要遍历矩阵所在行进行确认。
  2. 邻接表适合直接取所有与点相连的边,而不适合快速查看两个顶点的关系。
  3. 因此邻接矩阵和邻接表是相辅相成的,而综合来看的话,对于较为稀疏的图,即顶点相连的边较少,平分秋色,各有千秋,而对于稠密的完全图来说,邻接矩阵更为合适。因此我们下面统一采用临界矩阵的方式进行实现。

3.遍历方式

3.1广度优先遍历

  • 图解:
    在这里插入图片描述

我们再来分析一下流程,这里是以A为起点,进行广度遍历。

  1. 先遍历A,。
  2. 然后遍历与A相连的BCD。
  3. 其次在遍历与BCD相连的EF,此时就需要注意之前访问过的结点不能在接着继续访问了。
  4. 接着遍历与EF相连的HG,此时也需注意同样的问题。
  5. 最后遍历与H相连的I,此时同理。
  • 因此广度优先遍历,需注意访问的时候不能再访问已经访问过的结点,其次访问时越访问越深的。

实现方式:

  1. 采用队列的结构,不断入与队列元素相连的未访问的结点。
  2. 使用一个vector 记录结点是否已经被访问过了,当入队列时,即将对应的结点的下标标记为true。
void BFS(const V& src)
{int srci = GetVertexIndex(src);int n = _vertexs.size();vector<int> is_visited(n, false);//防止重复结点入队列,以免形成回路。queue<int> que;que.push(srci);is_visited[srci] = true;int levelsize = 1;//第一层就srci.while (!que.empty()){for (int i = 0; i < levelsize; i++){int front = que.front();que.pop();cout << front << ":" << _vertexs[front] << " ";//将与front相关的边进行入队列for (int i = 0; i < n; i++){if (_matrices[front][i] != W_MAX &&is_visited[i] == false){que.push(i);is_visited[i] = true;}}//这一层for循环式暴力遍历矩阵的所在行,确认是否有//没被访问的边。如果是邻接表就直接取较为方便,不过//稠密图倒是矩阵更优一点,能更好的确认两点的关系。					}cout << endl;//更新层结点的个数。levelsize = que.size();}
}
  • 测试用例:
	void TestBFS(){string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("A", "B", 1);g1.AddEdge("A", "C", 1);g1.AddEdge("A", "D", 1);g1.AddEdge("B", "E", 1);g1.AddEdge("B", "C", 1);g1.AddEdge("C", "F", 1);g1.AddEdge("C", "B", 1);g1.AddEdge("D", "F", 1);g1.AddEdge("E", "G", 1);g1.AddEdge("F", "H", 1);g1.AddEdge("H", "I", 1);g1.BFS("A");}
  • 运行结果:
    在这里插入图片描述

3.2深度优先遍历

  • 图解:
    在这里插入图片描述

我们再来分析一下流程,这里是以A为起点,进行深度遍历。

说明:已经访问过的结点我们是不再进行访问的。

  1. 先访问A相邻的B, 再访问与B相连的C, 再访问与C相连的F, 再访问与F相连的D。
  2. D相邻的A我们是不再进行访问的,因此又回到F, 接着访问H,紧接着访问与H相连的I,I没有访问过的结点,回退到H, H也没有访问过的结点回退到 F。
  3. F也没有与未访问的结点,回退到C,C也没有未访问的结点,于是回退到B。
  4. 接着访问与B相连的E, 更深一步访问与E相连的G,G没有未访问过的结点,回退到E, E此时也没有未访问过的结点回退到B, B此时也没有未访问过的结点,回退到A.
  5. 访问结束。
  • 实现代码:
	void _DFS(int srci,vector<bool>& is_visted){for (size_t i = 0; i < is_visted.size(); i++){if (_matrices[srci][i] != W_MAX && is_visted[i] == false){//此处打印的目的是便于测试。cout << "[" << _vertexs[srci] << "->" << _vertexs[i] << "]" << endl;is_visted[i] = true;_DFS(i, is_visted);}}}void DFS(const V& src){int srci = GetVertexIndex(src);vector<bool> is_visted(_vertexs.size(), false);is_visted[srci] = true;_DFS(srci,is_visted);}
  • 测试用例:
void TestDFS()
{string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("A", "B", 1);g1.AddEdge("A", "C", 1);g1.AddEdge("A", "D", 1);g1.AddEdge("B", "E", 1);g1.AddEdge("B", "C", 1);g1.AddEdge("C", "F", 1);g1.AddEdge("C", "B", 1);g1.AddEdge("D", "F", 1);g1.AddEdge("E", "G", 1);g1.AddEdge("F", "H", 1);g1.AddEdge("H", "I", 1);g1.DFS("A");
}/*主函数就自由发挥吧。*/
  • 运行结果:
    在这里插入图片描述

4.最小生成树

先来熟悉一下概念:

  • 最小生成树:图的生成树的路径最小。
  • 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的最小连通子图有n个顶点和n-1条边。
  • 连通图:若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
  • 注意:连通图是无向图的概念,也就是说最小生成树的图必须是无向的。强连通图才是有向图的定义。

 简单的说就是从由n个顶点组成的连通图中选择n-1条边,子图连通且所边的权值相加最小。

  实现方法下面介绍克鲁斯卡尔和普里姆两种算法。

4.1Kruskal算法

  • 原理
  1. 首先将所有的边管理起来,每次取出最小的边。
  2. 判断已经选出的边是否构环,如果构成就弃置再从中选最小的边。
  3. (n个顶点构成的图)选择n-1条边即可。
  • 实现关键
  1. 用优先级队列对边进行管理。
  2. 用并查集进行判环。
  • 实现代码:
/*
为方便读者进行阅读,此处博主贴了一份并查集的简略代码。
*/template<class T>class UnionFindSet{public://初始化大小,以及赋初值UnionFindSet(size_t size):_pPath(size, -1){}//将两个数进行合并void Union(int x1, int x2){//找两个数的父结点int index1 = find(x1);int index2 = find(x2);//如果相同则说明已经在同一个集合下,无需进行合并if (index1 == index2) return;//将小的和在大的身上(优化防止路径过长)if (_pPath[index1] < _pPath[index2]){swap(index1, index2);swap(x1, x2);}//此处保证index1的父节点的数量多,index2的数量小_pPath[index1] += _pPath[index2];_pPath[index2] = index1;}//找根int GetValueIndex(int x){//第一步:转换为下标int index = x;//第二步:根据下标找父节点while (_pPath[index] >= 0){index = _pPath[index];}//找到父路径进行返回。//路径压缩while (x != index){int parent = _pPath[x];_pPath[x] = index;x = parent;}return index;}int setsize(){int n = 0;for (int e : _pPath)if (e < 0) n++;return n;}private:vector<int> _pPath;};/*此结构体用于存放边的信息,放入优先级队列中便于进行管理。*/template<class W>struct Edge{int _srci;int _dsti;W _w;Edge(const int srci, const int dsti, const W& w):_srci(srci), _dsti(dsti), _w(w){}bool operator >(const Edge e) const{return _w > e._w;}};W Kruskal(self& min){min._vertexs = _vertexs;//第一步,用优先级队列存放所有的边priority_queue<Edge, vector<Edge>, greater<Edge>> minque;size_t n = _vertexs.size();//无向图,只需存放一半的图的信息即可。for (size_t i = 0; i < n; i++){for (size_t j = 0; j < i; j++){if (_matrices[i][j] != W_MAX){minque.push(Edge(i, j, _matrices[i][j]));}}}//第二步,选边,最小生成树,选择的边为 n-1条边size_t size = 0;UnionFindSet<int> u(n);W total = W();while (!minque.empty() && size != n-1){Edge top = minque.top();minque.pop();if (u.find(top._dsti) != u.find(top._srci)){//说明不构成环,选择此边,并将其加入到并查集和表中//此处是为了方便测试。cout << _vertexs[top._dsti] << "->" << _vertexs[top._srci]<< ":" << top._w << endl;u.Union(top._dsti, top._srci);min._AddEdge(top._dsti, top._srci, top._w);size++;total += top._w;}}//队列为空跳出循环,因此需要判断一下看是否选出了n-1条边。if (size != n - 1){//表明不能选出来return W();}return total;}
  • 测试用例:
	void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11);g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> kminTree(strlen(str));cout << "Kruskal:" << g.Kruskal(kminTree) << endl;}
/*main函数自由发挥吧*/
  • 运行结果:
    在这里插入图片描述

  • 图解:

在这里插入图片描述
说明:

  1. 程序走出的过程可能不一样,比如相同的边谁先选可能由优先级的实现原理决定,但大概率结果是一样的。
  2. 我们走出的只是局部的最优解,全局的最优解,可能还与相同的边的选择顺序有关,相同的边的如果互相影响,则可能会影响后面更大的边的选择。
  3. 因此如果所有的边互不相同那我们可以断定,此算法走出的最小生成树是确定的,即为全局的最小生成树。

4.2Prim算法

  • 原理
  1. 将顶点分为两个集合,设一个集合为X, 一个集合为Y。
  2. 选择一个起始点,放入X集合,剩余的顶点放入Y集合。
  3. 每次选择从Y中选择与X相连的最小的边,并将其相连的顶点放入X集合,从Y中丢弃此顶点。
  4. 直到选择 n - 1条边为止。
  • 实现关键:
  1. 将顶点分为两个集合X, Y,其实就避开了环的问题,产生环的原因本质就是一个集合内的两个顶点连到一块了。
  2. 我们选的是与集合X相连的最小的边,因此还要把X相连的边,放入优先级队列,往后循环可能会有一个集合内的边,我们只需判断边所连的目标顶点不在集合X即可,对于在集合X的我们不选即可。
  3. 除此之外,我们还需要确立一个起始点,用来初始化集合X和集合Y。
  • 实现代码:
	W Prim(self& min,const V& src){size_t n = _vertexs.size();min._vertexs = _vertexs;/*第一步:选择顶点,作为起始顶点。分为两个数组,一个为起始数组,一个为选边数组*/int srci = GetVertexIndex(src);vector<bool> X(n,false);vector<bool> Y(n,true);X[srci] = true;Y[srci] = false;//第二步:将与srci相关的边入队列中。priority_queue<Edge, vector<Edge>, greater<Edge>> minque;for (size_t i = 0; i < n; i++){//将边进行入队列if (_matrices[srci][i] != W_MAX){minque.push(Edge(srci, i, _matrices[srci][i]));}}//第三步进行选边W total = W();size_t size = 0;while (!minque.empty()){Edge front = minque.top();minque.pop();//判断边的终点是否在X中if (X[front._dsti]){//说明构成环。cout << "构成环:";cout << _vertexs[front._srci] << "->" << _vertexs[front._dsti] << endl;}else{cout << _vertexs[front._srci] << "->" << _vertexs[front._dsti] << endl;++size;total += front._w;//将边添加到最小生成树里面,并将与dsti相连的边入队列min._AddEdge(front._srci, front._dsti, front._w);//将desi所在的集合进行删除与添加Y[front._dsti] = false;X[front._dsti] = true;//将dsti所连的边进行入队列for (size_t i = 0; i < n; i++){//避免将已经入过的边再进行入队列if (_matrices[front._dsti][i] != W_MAX && Y[i]){//不在X[i] 即将在Y[i]进行入队列。minque.push(Edge(front._dsti, i,_matrices[front._dsti][i]));}}}}//如果不能生成最小生成树。if (size != n - 1){return W();}return total;}
  • 测试代码:
	void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11);g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> pminTree(strlen(str));cout << "Prim:" << g.Prim(pminTree, 'a') << endl;pminTree.Print();}
/*main 函数只需调用此函数即可*/

运行结果:

在这里插入图片描述

  • 图解:
    在这里插入图片描述

5.最短路径

  • 最短路径是描述两个顶点能连通的情况下,考虑两个顶点之间所经过路径的权值之和的最小值。
  • 举个例子,在现实世界中我们已经不关心两个地方能不能到的问题了,我们主要关系的是两个地方如何规划路程最短或者花费最低,诸如此类的问题,抽象到计算机即转换为了两个顶点所经过的路径的权值之和如何才能最短。

由此,我们引出迪杰斯特拉(Dijkstra), 贝尔曼福特(Bellman-Ford), 弗洛伊德(floyd warshall) 三种算法。

5.1Dijkstra算法

  • 基本认识
  • 此算法主要求的是不带负权值最小路径。

  • 算法思想主要在单源最短路径中进行体现。

  • 算法原理(贪心)
  1. 确定一个起始点,更新与其直接相连的顶点的路径。
  2. 选择路径和最短的那一个,此处确定了第一条路径最短的边。
  • 确定两字我们此处再稍作解释,由于已经选择了起始点直接到路径最短的顶点。因此不可能再出现,从起始点到另一个顶点再经过其它顶点到此点的路径和更短,更简单的表述是两点直接连着已经最短的了,再通过其它点绕远路只会更长,不会更短。
  • 此处用数学的语言进行描述或许更加直观。
  1. 再由最短的那个顶点,再更新(如果更小再进行更新)与其直接相连的边,再确定一条路径最短的边的顶点。由此顶点再进行更新。
  2. 如此往复,直到没有顶点可以更新,就结束。
  • 实现代码:
	void Dijkstra(const V& src, vector<W>& dst, vector<int>& pPath){//将边与路径进行初始化size_t n = _vertexs.size();int srci = GetVertexIndex(src);//值初始化为W_MAXdst.resize(n, W_MAX);//路径初始化为-1pPath.resize(n, -1);//src->src路径值初始化为W(),路径初始化为srcidst[srci] = W();pPath[srci] = srci;//创建一个bool的vector使得每个结点只访问一次vector<bool> is_visted(n, false);for (size_t i = 0; i < n; i++){W min = W_MAX;int vertexi = 0;//先选出没被访问过的最小的边for (size_t j = 0; j < n; j++){if (!is_visted[j] && dst[j] < min){min = dst[j];vertexi = j;}}//选出之后标记为选过的边is_visted[vertexi] = true;//再进行松弛更新与其相连的边for (size_t j = 0; j < n; j++){/*首先得有边,且是顶点没有访问的点,并且 srci->vertex + vertex->j < srci->j,再进行更新*/			if (_matrices[vertexi][j] != W_MAX && !is_visted[j]&& dst[vertexi] + _matrices[vertexi][j] < dst[j]){//更新j的父路径和srci->j的距离pPath[j] = vertexi;dst[j] = dst[vertexi] + _matrices[vertexi][j];}}}}
  • 此处对这里的pPath进行说明一下,是将路径进行压缩从二维降到了一维,但其实也很简单,本质与并查集的路径表示大致一样,下标存的是父节点的下标。
  • 另外,这里打印时因为每个结点表示的是父结点的下标,因此我们还需将路径倒着找到之后,再翻转成正向的,再进行打印。
  • 打印最短路径函数:
void PrinrtShotPath(const V& src, vector<W>& dst, vector<int>& pPath)
{int srci = GetVertexIndex(src);size_t n = _vertexs.size();//先找到路径再进行逆置for (size_t i = 0; i < n; i++){//不能是srci,要不然就陷入环了。if (i != srci){vector<int> path;int parent = i;while (parent != srci){path.push_back(parent);parent = pPath[parent];}//最后将srci根结点入进去path.push_back(srci);//逆转path得到路径reverse(path.begin(), path.end());for (auto index : path){cout << _vertexs[index] << "->";}//最后打印出路径值cout << "最短路径值为:" << dst[i] << endl;}}
}
  • 测试用例:
	void TestGraphDijkstra(){const char* str = "syztx";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('s', 't', 10);g.AddEdge('s', 'y', 5);g.AddEdge('y', 't', 3);g.AddEdge('y', 'x', 9);g.AddEdge('y', 'z', 2);g.AddEdge('z', 's', 7);g.AddEdge('z', 'x', 6);g.AddEdge('t', 'y', 2);g.AddEdge('t', 'x', 1);g.AddEdge('x', 'z', 4);vector<int> dist;vector<int> parentPath;g.Dijkstra('s', dist, parentPath);g.PrinrtShotPath('s', dist, parentPath);}
  • 运行结果:
    在这里插入图片描述

  • 图解:
    在这里插入图片描述

5.2Bellman-Ford算法

  • 用处:单源最短路径的负权值(不带负权回路)的图

  • 思想:暴力枚举遍历

  1. 由于只会更新出更短的路径,我们可以采取暴力枚举的方法。
  2. 将所有的边进行遍历,之后再遍历 n - 1 次进行修正。
  • 重点就在于: 为什么再遍历n - 1次 ?我们先来讨论一下,假设你再某次更新s->x->t->z 之后,s->x->t 出现了更短的路径(存在负权值,就有可能),更新成了s->y->t,但是原来已经更新的s->x->t->z虽然路径随着s->y->t更新,但是其s->t的权值并没有进行更新,这就导致了数据对不上的问题,因此我们需要再进行更新一轮,使之数据一致。而再次更新,有可能会导致其它最短路径的权值对不上,因此还要再进行更新,直到所有的最短路径都对上为止,因此最多要n-1次,带上最开始的那一次,总共n次。
  • 实现代码:
bool BellmanFord(const V& src, vector<W>& dst, vector<int>& pPath)
{//将边与路径进行初始化size_t n = _vertexs.size();int srci = GetVertexIndex(src);//值初始化为W_MAXdst.resize(n, W_MAX);//路径初始化为-1pPath.resize(n, -1);//src->src路径值初始化为W(),路径初始化为srcidst[srci] = W();pPath[srci] = srci;for (size_t k = 0; k < n; k++){//更新n轮,因为一个路径更新出更短的路径,会影响其它路径的权值,//因此需要再次更新。//一轮之后,更新出最短路径,则其它路径的权值需要暴力更新一遍。//不带第一轮,最多更新n-1轮->其中每一轮都更新出了最短路径。bool update = false;for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){//边存在,并且 s->i + i->j < s->j if (_matrices[i][j] != W_MAX && dst[i] + _matrices[i][j] < dst[j]){update = true;//更新父路径和权值pPath[j] = i;dst[j] = dst[i] + _matrices[i][j];}}}if (!update){break;}}//检查负权回路//再次更新一轮,检查是否能更新,如果还能更新,则存在负权回路。//如果没有更新,则为false,即bool is_existed = false;for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){//边存在,并且 s->i + i->j < s->j if (_matrices[i][j] != W_MAX && dst[i] + _matrices[i][j] < dst[j]){is_existed = true;}}}if (is_existed){return false;}return true;
}
  • 测试用例:
	void TestGraphBellmanFord(){const char* str = "syztx";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('s', 't', 6);g.AddEdge('s', 'y', 7);g.AddEdge('y', 'z', 9);g.AddEdge('y', 'x', -3);g.AddEdge('z', 's', 2);g.AddEdge('z', 'x', 7);g.AddEdge('t', 'x', 5);g.AddEdge('t', 'y', 8);g.AddEdge('t', 'z', -4);g.AddEdge('x', 't', -2);vector<int> dist;vector<int> parentPath;if (g.BellmanFord('s', dist, parentPath)){g.Print();g.PrinrtShotPath('s', dist, parentPath);}else{cout << "存在负权回路" << endl;}}
  • 运行结果:

在这里插入图片描述

  • 图解:

在这里插入图片描述

  • 说明:暴力更新,调试着看数据的变化效果更好。

  • 测试用例2:

	void TestGraphBellmanFord(){// 微调图结构,带有负权回路的测试const char* str = "syztx";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('s', 't', 6);g.AddEdge('s', 'y', 7);g.AddEdge('y', 'x', -3);g.AddEdge('y', 'z', 9);g.AddEdge('y', 'x', -3);g.AddEdge('z', 's', -2);//更改此处见效更明显。g.AddEdge('z', 'x', 7);g.AddEdge('t', 'x', 5);g.AddEdge('t', 'y', 8);g.AddEdge('t', 'z', -4);g.AddEdge('x', 't', -2);vector<int> dist;vector<int> parentPath;if (g.BellmanFord('s', dist, parentPath)){g.PrinrtShotPath('s', dist, parentPath);}else{cout << "存在负权回路" << endl;}}
  • 运行结果:
    在这里插入图片描述
  • 图解:
    在这里插入图片描述
    说明:暴力循环完之后,再更新一次又会引起其它变小,此种情况只会越更新越小,求不出最小路径!

5.3floyd warshall算法

  • 用处:多源最短路径的负权值(不带负权回路)的图

  • 算法思想(dp):

  1. 拆分子问题:分为两种情况
  1. 所有的边经过点K.
  2. 所有的边不经过点K.
  3. 这里的K可能是所有的顶点。
  4. 因此求前两种情况的所有情况的最小值即可。

图解:
在这里插入图片描述

  • 实现代码:
void FloydWarshall(vector<vector<W>>& vvdst, 
vector<vector<int>>& vvpPath)
{size_t n = _vertexs.size();//初始化dst与pPathvvdst.resize(n);vvpPath.resize(n);for (size_t i = 0; i < n; i++){vvdst[i].resize(n, W_MAX);vvpPath[i].resize(n, -1);}//再对边进行初始化,即将i直接到j的边先放在des数组中for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){if (_matrices[i][j] != W_MAX){vvdst[i][j] = _matrices[i][j];vvpPath[i][j] = i;}if (i == j){//与此同时由于是距离,所以i == j  即 i->i 的距离为0vvdst[i][j] = 0;}}}for (size_t k = 0; k < n; k++){//其中暴力选择k做为中间的边,分析是选择还是不选for (size_t i = 0; i < n; i++){//从中进行选则两端的边for (size_t j = 0; j < n; j++){//选择k作为中间的边,如果i->k,k->j < i->j//即分析是取k小还是不取k小,这里的k采用暴力枚举的方式。if (vvdst[i][k] != W_MAX && vvdst[k][j] != W_MAX&& vvdst[i][k] + vvdst[k][j] < vvdst[i][j]){//则需要更新dst[i][j]的父路径以及权值vvdst[i][j] = vvdst[i][k] + vvdst[k][j];/*i->k 更新 k->j,应为pPath[k][j]如果k->j中间没有其他结点,则说明 pPath[k][j] == k如果k->……->x->j中间经过了其它结点,则 pPath[k][j]==x*/vvpPath[i][j] = vvpPath[k][j];}}}}//此处我们打印出权值和路径的矩阵cout << "   ";for (size_t i = 0; i < n; i++){printf("%-3d", i);}cout << endl;//1.权值矩阵for (size_t i = 0; i < n; i++){printf("%-3d", i);for (size_t j = 0; j < n; j++){if (vvdst[i][j] == W_MAX){printf("%-3c", '*');}else{printf("%-3d", vvdst[i][j]);}}cout << endl;}printf("=============================================\n");//2.路径矩阵cout << "  ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;for (size_t i = 0; i < n; i++){cout << i << " ";for (size_t j = 0; j < n; j++){cout << vvpPath[i][j] << " ";}cout << endl;}
}
  • 测试用例:
	void TestFloydWarShall(){const char* str = "12345";Graph<char, int, INT_MAX, true> g(str, strlen(str));g.AddEdge('1', '2', 3);g.AddEdge('1', '3', 8);g.AddEdge('1', '5', -4);g.AddEdge('2', '4', 1);g.AddEdge('2', '5', 7);g.AddEdge('3', '2', 4);g.AddEdge('4', '1', 2);g.AddEdge('4', '3', -5);g.AddEdge('5', '4', 6);vector<vector<int>> vvDist;vector<vector<int>> vvParentPath;g.FloydWarshall(vvDist, vvParentPath);// 打印任意两点之间的最短路径for (size_t i = 0; i < strlen(str); ++i){g.PrinrtShotPath(str[i], vvDist[i], vvParentPath[i]);cout << endl;}}

运行结果:
在这里插入图片描述

  • 图解:
    在这里插入图片描述

说明:这里II的矩阵表示的数字是真实下标对应的数字,我们这里打印的父路径的矩阵表示的数字是下标,因此还需要对不为-1的数加上1才对的上。

总结

  1. 并查集的原理和基本实现。
  2. 图论的基本概念,存储结构(邻接表和邻接矩阵),遍历方式(广度优先和深度优先),最小生成树的两个算法,最短路径的三个算法。
  • 并查集是一个较为简单的数据结构,而图论的表示形式是较为抽象的,需要我们将实际的例子抽象处理,因此不太好理解,关键在于多调试,多画图

尾序

我是舜华,期待与你的下一次相遇!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/585119.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php-fpm运行一段时间,内存不足

目录 一&#xff1a;原因分析 二&#xff1a;解决 三:观察系统情况 php-fpm运行一段时间&#xff0c;内存不足&#xff0c;是什么原因呢。 一&#xff1a;原因分析 1:首先php-fpm的配置 &#xff08;1&#xff09;启动的进程数 启动的进程数越多,占用内存越高; 2:其次…

HarmonyOS自学-Day4(TodoList案例)

目录 文章声明⭐⭐⭐让我们开始今天的学习吧&#xff01;TodoList小案例 文章声明⭐⭐⭐ 该文章为我&#xff08;有编程语言基础&#xff0c;非编程小白&#xff09;的 HarmonyOS自学笔记&#xff0c;此类文章笔记我会默认大家都学过前端相关的知识知识来源为 HarmonyOS官方文…

1.NumPy 介绍

1.NumPy 介绍 1.1 NumPy 演变史 在 NumPy 之前&#xff0c;有两个 Python 数组包&#xff1a; Numeric 包 Numeric 包开发于 20 世纪 90 年代中期&#xff0c;在 Python 中提供了数组对象和数组感知函数。它由 C 语言编写&#xff0c;并与线性代数的标准快速实现相链接。它最…

nodejs微信小程序+python+PHP的林业信息管理系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

nodejs+vue+微信小程序+python+PHP的冷链物流配送系统-计算机毕业设计推荐

对于冷链物流信息调度系统所牵扯的管理及数据保存都是非常多的&#xff0c;例如管理员&#xff1b;首页、用户管理&#xff08;管理员、客户、业务员、配送员&#xff09;客户管理&#xff08;货物信息、客户运输单、车辆信息、调度安排&#xff09;这给管理者的工作带来了巨大…

Redisson依赖冲突记录

前言&#xff1a;项目使用的springboot项目为2.7.X 依赖冲突一&#xff1a;springboot 与 redisson版本冲突 项目中依赖了 Lock4j&#xff0c;此为苞米豆开源的分布式锁组件 <dependency><groupId>com.baomidou</groupId><artifactId>lock4j-redisso…

读算法霸权笔记07_筛选

1. 美国残疾人法案 1.1. 1990年 1.2. 公司在招聘时使用身体检查是非法的 1.3. 有某些心理健康问题的人被“亮了红灯”&#xff0c;他们因此没能找到一份正常的工作&#xff0c;过上正常的生活&#xff0c;这就使其进一步被社会孤立&#xff0c;而这正是《美国残疾人法案》要…

使用Halcon 采集图像并进行简单处理rgbl_to_gray/threshold/connection/fill_up

使用Halcon 采集图像并进行简单处理 文章目录 使用Halcon 采集图像并进行简单处理 下面介绍一个简单的采集图像的例子。在Halcon中利用图像采集接口&#xff0c;使用USB3.0相机实时拍摄图像。采集到图像后对图像进行简单的阀值分割处理&#xff0c;将有物体的区域标记出来。 &a…

3d光学轮廓仪测微光学器件应用及其重要意义

微光学器件是光学器件的重要分支&#xff0c;为光学通信、光传感、光计算等领域的发展提供重要支撑。微光学器件具有尺寸小、功耗低、低成本等优势&#xff0c;可以于电子器件集成&#xff0c;实现更高效的数据传输和信号处理。未来&#xff0c;随着微纳加工技术的进一步发展&a…

在 Golang 应用程序中管理多个数据库

掌握在 Golang 项目中处理多个数据库的艺术 在当前软件开发领域中&#xff0c;处理单个应用程序内的多个数据库的需求越来越普遍。具有强大功能的 Golang 是处理此类任务的绝佳解决方案&#xff0c;无论您是与多个数据源合作还是仅为增强组织和可扩展性而分隔数据。在本文中&a…

AI赋能金融创新:技术驱动的未来金融革命

人工智能&#xff08;AI&#xff09;作为一种技术手段&#xff0c;正逐渐改变金融行业的方方面面。从风险管理到客户体验&#xff0c;从交易执行到反欺诈&#xff0c;AI带来了许多创新和机遇。本文将探讨AI在金融领域的应用和其赋能的金融创新。 金融领域一直以来都面临着复杂的…

QT UI自动化测试(1)

一、框架选择 想结合公司产品搭建一套自动化测试框架&#xff0c;一方面自己学习用&#xff0c;一方面也希望跟公司业务结合起来&#xff0c;双赢。公司软件最多的产品是部署在Linux系统上&#xff0c;基于QT QML开发的UI&#xff0c;本来奔着免费的自动化框架去的&#xff0c;…

编写html的vscode快捷键

一快速生成 按住!(英文的)&#xff0c;回车。 二快捷键 1.代码格式化 用来对齐标签。整理代码&#xff0c;强迫症患者必备。 shiftaltf 2.快速移动一行 altdown altup 向上或向下移动一行 3.快速复制一行代码 ShiftAltUp ShiftAltDown 4.快速保存 Ctrl S 5.快速查…

ViT的极简pytorch实现及其即插即用

先放一张ViT的网络图 可以看到是把图像分割成小块&#xff0c;像NLP的句子那样按顺序进入transformer&#xff0c;经过MLP后&#xff0c;输出类别。每个小块是16x16&#xff0c;进入Linear Projection of Flattened Patches, 在每个的开头加上cls token和位置信息&#xff0c;…

Mysql5.7主从数据库同步失败(日记文件错误)解决记录

记录一次Mysql主从数据库同步失败(日记文件错误)解决记录 查看同步状态&#xff1a; 具体错误&#xff1a; 检查mysql数据库日记 2021-06-10T03:45:43.522398Z 1 [ERROR] Error reading packet from server for channel : event read from binlog did not pass crc check; the…

Oracle 拼接字符串

语法 使用||拼接如果内容中有单引号&#xff0c;则可在该单引号前面再加一个单引号进行转义 例子 比如有一个业务是根据需要生成多条插入语句 select insert into des_account_des_role(des_account_id, roles_id) values( || id || , || (select id from des_role where wo…

Ps:八大混合模式及其在色彩渲染上的运用

在所有的图层混合模式中&#xff0c;有八种比较特别。 特别之处在于&#xff0c;其它的混合模式在修改图层的“不透明度”或“填充”时&#xff0c;效果是一样的。 而这八种混合模式使用“填充”比使用“不透明度”可带来更好的效果&#xff0c;有时甚至可以说是惊艳。 提示&am…

ubuntu下编译obs-studio遇到的问题记录

参考的是这篇文档&#xff1a;Build Instructions For Linux obsproject/obs-studio Wiki GitHub 在安装OBS dependencies时&#xff0c; sudo apt install libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libswscale-d…

[ 云计算 | AWS ] 对比分析:Amazon SNS 与 SQS 消息服务的异同与选择

文章目录 一、前言二、Amazon SNS 服务&#xff08;Amazon Simple Notification Service&#xff09;三、Amazon SQS 服务&#xff08;Amazon Simple Queue Service&#xff09;四、SNS 与 SQS 的区别&#xff08;本文重点&#xff09;4.1 基于推送和轮询区别4.2 消费者数量对应…

HBuilder常用的快捷键

查看专栏目录 Network 灰鸽宝典专栏主要关注服务器的配置&#xff0c;前后端开发环境的配置&#xff0c;编辑器的配置&#xff0c;网络服务的配置&#xff0c;网络命令的应用与配置&#xff0c;windows常见问题的解决等。 文章目录 常用快捷键分9项快捷键1.文件(4)2.编辑(13)3.…