赞
踩
图的基本概念
图是由顶点集合和边的集合组成的一种数据结构,记作 G = ( V , E ) G=(V, E) G=(V,E) 。
有向图和无向图:
如下图:
完全图:
如下图:
邻接顶点:
顶点的度:
路径与路径长度:
带权图示例:
简单路径与回路:
如下图:
子图:
如下图:
连通图和强连通图:
生成树与最小生成树:
图的相关应用场景
图常见的表示场景如下:
关于有向图和无向图:
图的其他相关作用:
图与树的联系与区别
图与树的主要联系与区别如下:
图由顶点和边组成,存储图本质就是将图中的顶点和边存储起来。
邻接矩阵
邻接矩阵存储图的方式如下:
如下图:
说明一下:
邻接矩阵的优缺点
邻接矩阵的优点:
邻接矩阵的缺点:
邻接矩阵的实现
邻接矩阵所需成员变量:
邻接矩阵的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //构造函数 Graph(const V* vertexs, int n) :_vertexs(vertexs, vertexs + n) //设置顶点集合 ,_matrix(n, vector<int>(n, MAX_W)) { //开辟二维数组空间 //建立顶点与下标的映射关系 for (int i = 0; i < n; i++) { _vIndexMap[vertexs[i]] = i; } } //获取顶点对应的下标 int getVertexIndex(const V& v) { auto iter = _vIndexMap.find(v); if (iter != _vIndexMap.end()) { //顶点存在 return iter->second; } else { //顶点不存在 throw invalid_argument("不存在的顶点"); return -1; } } //添加边 void addEdge(const V& src, const V& dst, const W& weight) { int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //获取源顶点和目标顶点的下标 _matrix[srci][dsti] = weight; //设置邻接矩阵中对应的值 if (Direction == false) { //无向图 _matrix[dsti][srci] = weight; //添加从目标顶点到源顶点的边 } } //打印顶点集合和邻接矩阵 void print() { int n = _vertexs.size(); //打印顶点集合 for (int i = 0; i < n; i++) { cout << "[" << i << "]->" << _vertexs[i] << endl; } cout << endl; //打印邻接矩阵 //横下标 cout << " "; for (int i = 0; i < n; i++) { //cout << i << " "; printf("%4d", i); } cout << endl; for (int i = 0; i < n; i++) { cout << i << " "; //竖下标 for (int j = 0; j < n; j++) { if (_matrix[i][j] == MAX_W) { printf("%4c", '*'); } else { printf("%4d", _matrix[i][j]); } } cout << endl; } cout << endl; } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
邻接表
邻接表存储图的方式如下:
如下图:
说明一下:
邻接表的优缺点
邻接表的优点:
邻接表的缺点:
邻接表的实现
链表结点所需成员变量:
代码如下:
//邻接表 namespace LinkTable { //链表结点定义 template<class W> struct Edge { //int _srci; //源顶点的下标(可选) int _dsti; //目标顶点的下标 W _weight; //边的权值 Edge<W>* _next; //连接指针 Edge(int dsti, const W& weight) :_dsti(dsti) ,_weight(weight) ,_next(nullptr) {} }; }
说明一下:
邻接表所需成员变量:
邻接表的实现:
代码如下:
//邻接表 namespace LinkTable { template<class V, class W, bool Direction = false> class Graph { typedef Edge<W> Edge; public: //构造函数 Graph(const V* vertexs, int n) :_vertexs(vertexs, vertexs + n) //设置顶点集合 , _linkTable(n, nullptr) { //开辟邻接表的空间 //建立顶点与下标的映射关系 for (int i = 0; i < n; i++) { _vIndexMap[vertexs[i]] = i; } } //获取顶点对应的下标 int getVertexIndex(const V& v) { auto iter = _vIndexMap.find(v); if (iter != _vIndexMap.end()) { //顶点存在 return iter->second; } else { //顶点不存在 throw invalid_argument("不存在的顶点"); return -1; } } //添加边 void addEdge(const V& src, const V& dst, const W& weight) { int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //获取源顶点和目标顶点的下标 //添加从源顶点到目标顶点的边 Edge* sdEdge = new Edge(dsti, weight); sdEdge->_next = _linkTable[srci]; _linkTable[srci] = sdEdge; if (Direction == false) { //无向图 //添加从目标顶点到源顶点的边 Edge* dsEdge = new Edge(srci, weight); dsEdge->_next = _linkTable[dsti]; _linkTable[dsti] = dsEdge; } } //打印顶点集合和邻接表 void print() { int n = _vertexs.size(); //打印顶点集合 for (int i = 0; i < n; i++) { cout << "[" << i << "]->" << _vertexs[i] << " "; } cout << endl << endl; //打印邻接表 for (int i = 0; i < n; i++) { Edge* cur = _linkTable[i]; cout << "[" << i << ":" << _vertexs[i] << "]->"; while (cur) { cout << "[" << cur->_dsti << ":" << _vertexs[cur->_dsti] << ":" << cur->_weight << "]->"; cur = cur->_next; } cout << "nullptr" << endl; } } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<Edge*> _linkTable; //邻接表(出边表) }; }
说明一下:
图的遍历指的是遍历图中的顶点,主要有广度优先遍历和深度优先遍历两种方式。
广度优先遍历
广度优先遍历又称BFS,其遍历过程类似于二叉树的层序遍历,从起始顶点开始一层一层向外进行遍历。
如下图:
广度优先遍历的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //广度优先遍历 void bfs(const V& src) { int srci = getVertexIndex(src); //起始顶点的下标 queue<int> q; //队列 vector<bool> visited(_vertexs.size(), false); //标记数组 q.push(srci); //起始顶点入队列 visited[srci] = true; //起始顶点标记为访问过 while (!q.empty()) { int front = q.front(); q.pop(); cout << _vertexs[front] << " "; for (int i = 0; i < _vertexs.size(); i++) { //找出从front连接出去的顶点 if (_matrix[front][i] != MAX_W && visited[i] == false) { //是邻接顶点,并且没有被访问过 q.push(i); //入队列 visited[i] = true; //标记为访问过 } } } cout << endl; } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
深度优先遍历
深度优先遍历又称DFS,其遍历过程类似于二叉树的先序遍历,从起始顶点开始不断对顶点进行深入遍历。
如下图:
深度优先遍历的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //深度优先遍历(子函数) void _dfs(int srci, vector<bool>& visited) { cout << _vertexs[srci] << " "; //访问 visited[srci] = true; //标记为访问过 for (int i = 0; i < _vertexs.size(); i++) { //找从srci连接出去的顶点 if (_matrix[srci][i] != MAX_W && visited[i] == false) { //是邻接顶点,并且没有被访问过 _dfs(i, visited); //递归遍历 } } } //深度优先遍历 void dfs(const V& src) { int srci = getVertexIndex(src); //起始顶点的下标 vector<bool> visited(_vertexs.size(), false); //标记数组 _dfs(srci, visited); //递归遍历 cout << endl; } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
最小生成树
关于最小生成树:
说明一下:
构成最小生成树的准则
构造最小生成树的准则如下:
构造最小生成树的算法有Kruskal算法和Prim算法,这两个算法都采用了逐步求解的贪心策略。
Kruskal算法(克鲁斯卡尔算法)
Kruskal算法的基本思想如下:
动图演示:
Kruskal算法的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //强制生成默认构造 Graph() = default; void _addEdge(int srci, int dsti, const W& weight) { _matrix[srci][dsti] = weight; //设置邻接矩阵中对应的值 if (Direction == false) { //无向图 _matrix[dsti][srci] = weight; //添加从目标顶点到源顶点的边 } } //添加边 void addEdge(const V& src, const V& dst, const W& weight) { int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //获取源顶点和目标顶点的下标 _addEdge(srci, dsti, weight); } //边 struct Edge { int _srci; //源顶点的下标 int _dsti; //目标顶点的下标 W _weight; //边的权值 Edge(int srci, int dsti, const W& weight) :_srci(srci) , _dsti(dsti) , _weight(weight) {} bool operator>(const Edge& edge) const{ return _weight > edge._weight; } }; //获取当前图的最小生成树(Kruskal算法) W Kruskal(Graph<V, W, MAX_W, Direction>& minTree) { int n = _vertexs.size(); //设置最小生成树的各个成员变量 minTree._vertexs = _vertexs; //设置最小生成树的顶点集合 minTree._vIndexMap = _vIndexMap; //设置最小生成树顶点与下标的映射 minTree._matrix.resize(n, vector<W>(n, MAX_W)); //开辟最小生成树的二维数组空间 priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap; //优先级队列(小堆) //将所有边添加到优先级队列 for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { //只遍历矩阵的一半,避免重复添加相同的边 if (_matrix[i][j] != MAX_W) minHeap.push(Edge(i, j, _matrix[i][j])); } } UnionFindSet ufs(n); //n个顶点的并查集 int count = 0; //已选边的数量 W totalWeight = W(); //最小生成树的总权值 while (!minHeap.empty() && count < n - 1) { //从优先级队列中获取一个权值最小的边 Edge minEdge = minHeap.top(); minHeap.pop(); int srci = minEdge._srci, dsti = minEdge._dsti; W weight = minEdge._weight; if (!ufs.inSameSet(srci, dsti)) { //边的源顶点和目标顶点不在同一个集合 minTree._addEdge(srci, dsti, weight); //在最小生成树中添加边 ufs.unionSet(srci, dsti); //合并源顶点和目标顶点对应的集合 count++; totalWeight += weight; cout << "选边: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl; } else { //边的源顶点和目标顶点在同一个集合,加入这条边会构成环 cout << "成环: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl; } } if (count == n - 1) { cout << "构建最小生成树成功" << endl; return totalWeight; } else { cout << "无法构成最小生成树" << endl; return W(); } } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
Prim算法(普里姆算法)
Prim算法的基本思想如下:
动图演示:
Prim算法的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //边 struct Edge { int _srci; //源顶点的下标 int _dsti; //目标顶点的下标 W _weight; //边的权值 Edge(int srci, int dsti, const W& weight) :_srci(srci) , _dsti(dsti) , _weight(weight) {} bool operator>(const Edge& edge) const{ return _weight > edge._weight; } }; //获取当前图的最小生成树(Prim算法) W Prim(Graph<V, W, MAX_W, Direction>& minTree, const V& start) { int n = _vertexs.size(); //设置最小生成树的各个成员变量 minTree._vertexs = _vertexs; //设置最小生成树的顶点集合 minTree._vIndexMap = _vIndexMap; //设置最小生成树顶点与下标的映射 minTree._matrix.resize(n, vector<W>(n, MAX_W)); //开辟最小生成树的二维数组空间 int starti = getVertexIndex(start); //获取起始顶点的下标 vector<bool> forest(n, false); forest[starti] = true; priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap; //优先级队列(小堆) //将从起始顶点连接出去的边加入优先级队列 for (int i = 0; i < n; i++) { if (_matrix[starti][i] != MAX_W) minHeap.push(Edge(starti, i, _matrix[starti][i])); } int count = 0; //已选边的数量 W totalWeight = W(); //最小生成树的总权值 while (!minHeap.empty() && count < n - 1) { //从优先级队列中获取一个权值最小的边 Edge minEdge = minHeap.top(); minHeap.pop(); int srci = minEdge._srci, dsti = minEdge._dsti; W weight = minEdge._weight; if (forest[dsti] == false) { //边的目标顶点还没有被加入到forest集合中 //将从目标顶点连接出去的边加入优先级队列 for (int i = 0; i < n; i++) { if (_matrix[dsti][i] != MAX_W && forest[i] == false) //加入的边的目标顶点不能在forest集合中 minHeap.push(Edge(dsti, i, _matrix[dsti][i])); } minTree._addEdge(srci, dsti, weight); //在最小生成树中添加边 forest[dsti] = true; //将边的目标顶点加入forest集合 count++; totalWeight += weight; cout << "选边: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl; } else { //边的目标顶点已经在forest集合中,加入这条边会构成环 cout << "成环: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl; } } if (count == n - 1) { cout << "构建最小生成树成功" << endl; return totalWeight; } else { cout << "无法构成最小生成树" << endl; return W(); } } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
最短路径
关于最短路径:
Dijkstra算法(迪杰斯特拉算法)
使用前提:图中所有边的权值非负。
Dijkstra算法的基本思想如下:
动图演示:
Dijkstra算法的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //获取单源最短路径(Dijkstra算法) void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath) { int n = _vertexs.size(); int srci = getVertexIndex(src); //获取源顶点的下标 dist.resize(n, MAX_W); //各个顶点的估计值初始化为MAX_W parentPath.resize(n, -1); //各个顶点的前驱顶点初始化为-1 dist[srci] = W(); //源顶点的估计值设置为权值的缺省值 vector<bool> S(n, false); //已经确定最短路径的顶点集合 for (int i = 0; i < n; i++) { //将Q集合中的n个顶点全部加入到S集合 //从集合Q中选出一个估计值最小的顶点 W minW = MAX_W; //最小估计值 int u = -1; //估计值最小的顶点 for (int j = 0; j < n; j++) { if (S[j] == false && dist[j] < minW) { minW = dist[j]; u = j; } } S[u] = true; //将选出的顶点加入到S集合 //对u连接出去的顶点进行松弛更新 for (int v = 0; v < n; v++) { if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]) { //松弛的顶点不能在S集合 dist[v] = dist[u] + _matrix[u][v]; //松弛更新出更小的估计值 parentPath[v] = u; //更新路径的前驱顶点 } } } } //打印最短路径及路径权值 void printShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath) { int n = _vertexs.size(); int srci = getVertexIndex(src); //获取源顶点的下标 for (int i = 0; i < n; i++) { vector<int> path; int cur = i; while (cur != -1) { //源顶点的前驱顶点为-1 path.push_back(cur); cur = parentPath[cur]; } reverse(path.begin(), path.end()); //逆置 for (int j = 0; j < path.size(); j++) { cout << _vertexs[path[j]] << "->"; } cout << "路径权值: " << dist[i] << "" << endl; } } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
Dijkstra算法的原理
Bellman-Ford算法(贝尔曼福特算法)
Bellman-Ford算法的基本思想如下:
Bellman-Ford算法的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //获取单源最短路径(BellmanFord算法) bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath) { int n = _vertexs.size(); int srci = getVertexIndex(src); //获取源顶点的下标 dist.resize(n, MAX_W); //各个顶点的估计值初始化为MAX_W parentPath.resize(n, -1); //各个顶点的前驱顶点初始化为-1 dist[srci] = W(); //源顶点的估计值设置为权值的缺省值 for (int k = 0; k < n - 1; k++) { //最多更新n-1轮 bool update = false; //记录本轮是否更新过 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { dist[j] = dist[i] + _matrix[i][j]; //松弛更新出更小的路径权值 parentPath[j] = i; //更新路径的前驱顶点 update = true; } } } if (update == false) { //本轮没有更新过,不必进行后续轮次的更新 break; } } //更新n-1轮后如果还能更新,则说明带有负权回路 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) { return false; //带有负权回路的图无法求出最短路径 } } } return true; } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
为什么最多进行 n − 1 n-1 n−1 轮松弛更新?
从一个顶点到另一个顶点的最短路径中不能包含回路:
在每一轮松弛过程中,后面路径的更新可能会影响到前面已经更新过的路径,比如使得前面已经更新过的路径的长度可以变得更短,或者使得某些源顶点之前不可达的顶点变得可达,但每一轮松弛至少能确定最短路径中的一条边,如果图中有 n n n 个顶点,那么两个顶点之间的最短路径最多有 n − 1 n-1 n−1 条边,因此最多需要进行 n − 1 n-1 n−1 次松弛更新。
例如下图中,顶点 A A A、 B B B、 C C C、 D D D、 E E E 的下标分别是0、1、2、3、4,现在要计算以顶点 E E E 为源顶点的单源最短路径。
对于上述图来说,Bellman-Ford算法在第一轮松弛的时候只能更新出 E − > D E->D E−>D 这条边,在第二轮的时候只能更新出 D − > C D->C D−>C ,以此类推,最终就会进行4轮松弛更新(建议通过代码调试观察)。
说明一下:
Floyd-Warshall算法(弗洛伊德算法)
Floyd-Warshall算法的基本思想如下:
Floyd-Warshall算法的实现:
代码如下:
//邻接矩阵 namespace Matrix { template<class V, class W, W MAX_W = INT_MAX, bool Direction = false> class Graph { public: //获取多源最短路径(FloydWarshall算法) void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvParentPath) { int n = _vertexs.size(); vvDist.resize(n, vector<W>(n, MAX_W)); //任意两个顶点直接的路径权值初始化为MAX_W vvParentPath.resize(n, vector<int>(n, -1)); //各个顶点的前驱顶点初始化为-1 //根据邻接矩阵初始化直接相连的顶点 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (_matrix[i][j] != MAX_W) { //i->j有边 vvDist[i][j] = _matrix[i][j]; //i->j的路径权值 vvParentPath[i][j] = i; //i->j路径的前驱顶点为i } if (i == j) { //i->i vvDist[i][j] = W(); //i->i的路径权值设置为权值的缺省值 } } } for (int k = 0; k < n; k++) { //依次取各个顶点作为i->j路径的中间顶点 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][k] + vvDist[k][j] < vvDist[i][j]) { //存在i->k和k->j的路径,并且这两条路径的权值之和小于当前i->j路径的权值 vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; //松弛更新出更小的路径权值 vvParentPath[i][j] = vvParentPath[k][j]; //更小路径的前驱顶点 } } } } } private: vector<V> _vertexs; //顶点集合 unordered_map<V, int> _vIndexMap; //顶点映射下标 vector<vector<W>> _matrix; //邻接矩阵 }; }
说明一下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。