1、图的遍历
图的遍历是指从图中的某一个顶点出发,按照一定的策略将图中的所有顶点访问一一遍,使得每个顶点被遍历且都遍历一次。容易看出,树是一种特殊的图,所以树的遍历实际上也是一种特殊的图的遍历,图的遍历是树的遍历的一般化形式。
图的遍历比树的遍历复杂,因为图中的每一个顶点都可能与其他顶点相连。也就是说,在访问过某个顶点之后,还是可能经过其他路径之后这个顶点。为了避免重复访问一个顶点,在遍历图的过程中应当判断顶点是否被访问,若已被访问则不再访问。
图的遍历有广度优先遍历和深度优先遍历两种基本形式,对于无向图和有向图都适用。
1.1 深度优先遍历
图的深度优先遍历(DFS)类似于树的先根遍历。深度优先遍历的特点是尽可能先对纵深方向探索。
对于给定的图G(V,E),初始状态是V中所有顶点都未被访问。首先,选取一个顶点开始遍历。设
为源点,访问顶点v0并标记为VISITED,接着访问与v0邻接的未被访问过的顶点v1,再从v1出发递归地按照深度优先的方式继续遍历。当遇到一个所有邻接顶点都被访问过的w之后,则回到已访问顶点序列存在未访问邻接顶点的u,再从u出发按照递归的方式按照深度优先的方式遍历。重复上述过程,直至v0出发的所有顶点都被检测过为止。此时,图中从源点开始有路径可达的顶点都被访问过。若G是连通图,则访问结束;否则,选择一个尚未被访问的顶点作为新的源点进行深度优先遍历。事实上深度优先遍历的结果是沿着图的某一分支搜索,直至它的末端,然后回溯,沿着另一分支进行同样的搜索,以此类推。下面给出图的深度优先遍历算法:
void DFS(Graph &G, int v) { //深度优先搜索递归实现
G.Mark[v] == VISITED; //将标记位置设置为已访问
visit(G,v); //访问顶点v
for (e = FirstEdge(G, v); G.IsEdge(e); e = G.NextEdge(e)) {
if (G.Mark[G.ToVertex(e)] == UNVISITED) {
DFS(G, G.ToVextex(e));
}
周游图的过程实质上是搜索每个顶点的邻接点的过程,时间主要耗费在从顶点出发搜索它的所有邻接顶点上。分析上面算法,对于具有n个顶点和e条边的无向图或者有向图来说,深度优先遍历对图中每个顶点至多调用一次DFS()函数。用邻接矩阵表示图时,共需检查n^2个矩阵元素,所需时间O(n^2);用邻接表表示图时,找邻接点需将邻接表中所有的边顶点检查一遍,需要时间O(e),对应的深度优先算法的时间复杂度是O(n+e)。
1.2 广度优先遍历
系统的访问图的所有顶点的另一个方法是广度优先遍历(BFS)。其遍历过程是:从某个顶点v出发,横向搜索所有邻接点u1,u2,...,u9999。在依次访问v各个邻接点后,再从这些邻接点出发,依次访问未曾访问过的邻接点。重复上述过程直至图中所有与源点v有路径相同的顶点都被访问为止。若图G为连通图,则遍历结束;否则,在图G中选择一个未被访问的顶点作为新源点进行广度优先遍历。
广度优先遍历类似于树的层次遍历。可以使用FIFO队列保存已经访问过的顶点,从而使得先访问的顶点的邻接点在访问过程中下一轮优先被访问。在遍历过程中,每访问到一个顶点后将其入队,当队头元素出队时将其未被访问的邻接点入队,每个顶点只入队一次。
图的广度优先遍历算法实现:
void BFS(Graph &G, int v) {
using std :: queue; //使用STL中的队列
queue <int> Q;
visit(G, v); //访问顶点v
G.Mark[v] = VISITED; //将标记设置为VISITED
Q.push(v); //顶点v入队列
if (!IsEmpty(Q)) {
int u = Q.front(); //获取队列头部元素
Q.pop(); //队列头部元素出队
for (Edge e = FirstEdge(u); G.IsEdge(e); e = G.NextEdge(e)) {
if (G.Mark[G.ToVertex(e)] == UNVISITED) {
visit(G, G.ToVertex(e);
G.Mark[G.ToVertex(e)] == VISITED;
Q.push(G.ToVertex(e));
}
广度优先遍历实质与深度优先遍历相同,只是访问顺序不同,二者的时间复杂度相同。
2、拓扑排序
有向图的边可以看作顶点之间制约关系的描述。在工程实践中,有些工程经常受到一定条件的约束,例如一个工程项目通常由若干个子工程组成,某些子工程完成之后某些子工程才能进行。
一个有向无环的图称为有向无环图(DAG)。有向无环图常用来描述一个过程和一个系统的进行过程。若用有向无环图表示一个工程,其顶点表示活动,用有向边<Vi, Vj>表示活动Vi必须先于活动Vj进行的这样一种关系,则将这种有向图称为顶点表示的网络,简称AOV网。
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序。
(1)每个顶点出现且只出现一次。
(2)若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径。
如图所示的有向无环图进行拓扑排序可以得到拓扑序列{c0, c1, c2, c3, c4, c5, c7, c8, c6},也可以得到{c1, c4, c5, c0, c7, c8, c2, c3, c6}等。当然,一个有向无环图顶点的拓扑序列也不是唯一的。
进行有向图拓扑排序方法如下:
(1)从有向图中选出一个没有前驱(入度为0)的顶点并输出。
(2)删除图的该顶点和所有以它为始点的弧。
不断进行上述两个步骤,会出现两种情形;要么有向图中的顶点全部被输出,要么当前图中不存在没有前驱的顶点。当图中的顶点全部输出时,就完成了有向无环图的拓扑排序;当图中还有顶点未输出时,说明有向图中含有环。因此,拓扑排序就可以检查图中是否存在环。
下面用邻接表作为有向图的存储结构来实现有向图的拓扑排序。每个顶点中加入一个存放该顶点的入度的域。这样,检查顶点数组就可以方便地找出入度为0的顶点,即没有前驱的顶点。删除该顶点及以它为尾的弧,即将边表中所有弧头顶点的入度减1。
为了查找度为0的顶点的次数,可把入度为0的顶点构成一个队列,使得每次查找度为0的顶点时只要从队列中取出第一个顶点即可,而不必检查整个顶点表。删除入度为0的顶点,如果此时某个顶点的入度减为0,就将其入队。
//队列实现拓扑排序
void TopsortQueue(Graph &G) {
for (int i = 0; i < G.VerticsNum(); i++) { //初始化Mark数组
G.Mark[i] = UNVISITED;
}
using std :: queue; //使用STL中的数列
queue <int> Q;
for (i = 0; i < G.VerticsNum(); i++) {
if (G.Indegree[e] == 0) { //如果队列的入度为0,就将其入队
Q.push(i);
}
}
while (!G.IsEmpty(Q)) { //如果队列非空
int v = Q.front(); //获取队列头部元素
Q.pop(); //访问顶点v
Visit = (G, v); //将标记位置设置为VISITED
for (Edge e = FirstEdge(v); G.IsEdge(e); e = G.NextEdge()) {
G.Indegree(G.ToVertex(e))--; //与该顶点相连的顶点入度减1
if (G.Indegree(G.ToVertex(e)) == 0) { //如果顶点入度减为0,则入队
Q.push(G.ToVertex(e));
}
}
}
for (i = 0; i < G.VerticsNum(); i++) { //利用标记为判断图中是否有环
if (G.Mark[i] == UNVISITED) {
cout << "此图有环";
break;
}
}
}
3、最短路径
当图是带权图时,把从一个顶点v0到图中任意一个顶点vi的一条路径所经过边上的权值之和,定义为该路径的带权路径长度。一般称路径上的第一个顶点称为源点,最后一个顶点称为汇点,或终点。我们把带权路径长度最短的那条路径称为最短路径。
求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。带权有向图G的带权路径问题一遍可分为两类:一是单源最短路径,即求图中的某一顶点到其他各顶点的最短路径,可通过Dijkstra算法求解;二是求每对顶点间的最短路径,可通过Floyd算法来求解。
3.1 Dijsktra算法
给定一个带权图G = <V, E>, 其中每条边(vi, vj)上的权W[vi, vj]是一个非负实数。另外,给定V中的一个顶点s充当源点。现在要计算从源点s到所有其他各顶点的最短路径,这个问题通常称为单源最短路径问题。
解决单源最短路径问题的一个常用的算法是Dijkstra算法,这是一种按路径长度递增的次序产生到各顶点最短路径的贪心算法。它的基本思想是:把图的顶点集分成两个集合V和V-S。第一个集合S表示最短距离已经确定的顶点集,其余的顶点放在另一个集合V-S中。初始时,集合S只包含源点,即S = {s},此时只有源点到自己的距离是已知的。设v是V中某个顶点,把从源点s到顶点v且中间只经过集合S的路径称为从源点到v的最短路径,并用数组D来记录当前所找到的从源点s到每个顶点的最短特殊路径长度。D的初始状态为:如果从源点s到顶点v有弧,则D[v]为弧的权值;否则将D[v]置为无穷大。Dijsktra算法每次从尚未确定最短路径长度的集合V-S中选取一个最短特殊路径长度最小的顶点u,将u加入集合S,同时修改数组D中可由s可达的最短路径长度:若加进u作为中间顶点,使得vi的最短特殊路径长度变短,则修改vi的距离值(时,令
)。然后,重复上述操作,一旦S包含了所有V中顶点,D中的各顶点的距离值就记录了从源点s到该顶点上的最短路径长度。
距离数组中还可以设立一个域来记录从源点到顶点v的最短路径上v前面经过的一个顶点,这样就可以推导出最短路径。初始时,对所有的v!=s,均设置其前一个顶点为s。在用Dijkstra算法更新最短路径长度时,只要D[u]+W[u,v],就设置v 的前一个顶点为u,否则不做修改。当Dijkstra算法终止时就可以找到源点s到顶点v的最短路径。
下面给出Dijsktra算法的代码:
#include <iostream>
#include "Graph.h"
#include "MinHeap.h"
class Dist { //用于保存最短路径信息
public:
int index; //顶点的索引值,仅Dijkstra和Floyd算法用于保存路径信息
int length; //当前的最短路径长度
int pre; //路径最后经过的顶点
};
class Graph {
public:
Graph(int size); // 构造函数
void AddEdge(int start, int end, int weight); // 添加边
int VerticesNum(); // 返回顶点数量
bool IsEdge(int start, int end); // 判断是否存在边
int Weight(int start, int end); // 返回边的权重
Edge FirstEdge(int v); // 返回以顶点v为起点的第一条边
Edge NextEdge(Edge e); // 返回与e相同起点的下一条边
};
template <class T>
class MinHeap {
public:
MinHeap(int capacity); // 构造函数
void Insert(T value); // 插入元素
T RemoveMin(); // 移除最小值元素
bool IsEmpty(); // 判断堆是否为空
};
void Dijkstra(Graph &G, int s, Dist*&D) {
D = new Dist[G.VerticesNum()];
for (int i = 0; i < G.VerticesNum(); i++) {
G.Mark[i] = UNVISTED;
D[i].index = i; //顶点的索引值
D[i].length = INFINITE; //当前最短路径的长度
D[i].pre = s; //路径最后经过的顶点
}
D[s].length = 0; //源点到自身的路径长度设置为0
MinHeap <Dist> H(G.EdgesNum()); //最小值堆用于找出最短路径
H.Insert(D[s]); //将s插入到最小值堆中
bool FOUND = false; // 定义并初始化FOUND
Dist d;
while (!H.IsEmpty()) {
d = H.RemoveMin(); //移除最小值元素
if (G.Mark[d.index] == UNVISTED) {
FOUND = true;
break; // 跳出while循环
}
}
if (!FOUND)
break; //若没有符合条件的最短路径则跳出本次循环
if (FOUND) { // 如果有符合条件的最短路径
int v = d.index;
G.Mark[v] = VISITED;
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = NextEdge(e)) {
if (D[G.ToVertex(e)].length > (D[v].length + G.Weight(e))) {
D[G.ToVertex(e)].length = D[v].length + G.Weight(e);
D[G.ToVertex(e)].pre = v;
H.Insert(G.ToVertex(e));
}
}
}
}
int main() {
// 创建一个示例图
Graph G(5); // 假设图中有5个顶点
G.AddEdge(0, 1, 2); // 添加边,参数分别为起点、终点、权重
G.AddEdge(0, 2, 4);
G.AddEdge(1, 2, 1);
G.AddEdge(1, 3, 7);
G.AddEdge(2, 3, 3);
G.AddEdge(2, 4, 2);
G.AddEdge(3, 4, 5);
int s = 0; // 指定源点为顶点0
Dist* D;
Dijkstra(G, s, D);
// 打印各顶点到源点的最短路径信息
for (int i = 0; i < G.VerticesNum(); i++) {
std::cout << "Vertex " << i << ": ";
std::cout << "Shortest distance = " << D[i].length << ", ";
std::cout << "Predecessor = " << D[i].pre << std::endl;
}
// 释放内存
delete[] D;
return 0;
}
对于n个顶点e条边的图,图中的任何一条边都可能在最短路径中出现,因此最短路径算法对每条边至少都要检查一次。上述算法采用最小堆来选择权值最小的边,因此每次改变最短特殊长度时需要对堆进行一次重排,此时的时间复杂度是O((n+e)loge),适合于稀疏图。如果通过直接比较D数组元素,确定代价最小的边就需要总时间O(n^2);取出最短特殊路径长度最小的顶点后,修改最短路径长度共需要时间O(e),因此共需要花费O(n^2),这种方法适合稠密图。
3.2 Floyd算法
给定一个带权图G = <V,E>,其中每条边(vi, vj)上的权W[vi, vj]是一个非负实数。要求计算对任意的顶点有序对<vi, vj>找出从顶点vi到顶点vj的最短路径。这个问题常被称为带权的图的所有顶点对之间的最短路径问题。
解决这一问题可以每次以一个顶点为源点,重复执行Dijsktra算法n次,这样就可以求得所有顶点对之间的最短路径及其最短路径长度,其时间复杂度为O(n^3)。
下面介绍求所有顶点对之间比较直接的Floyd算法,这是一个典型的动态规划问题,先自底向上求解子问题的解,然后由这些子问题的解得到原问题的解。这个算法的时间复杂度也是O(n^3),但形式上较为简单。
Floyd算法用相邻矩阵adj来表示带权有向图,该算法的基本思想是:初始化为相邻矩阵adj,在矩阵
上做n次迭代,递归地产生一个矩阵序列
。其中,经过第k次迭代,
的值等于从顶点vi到顶点vj中间顶点的序号不大于k的最短路径长度。由于进行第k次迭代已求得矩阵
,那么从顶点vi到顶点vj中间顶点的序号不大于k的最短路径有两种情况:一种是中间不经过顶点vk,此时有
;另一种是中间经过顶点vk,此时
,这条由顶点vi经过vk到顶点vj的中间顶点序号不大于k的最短路径由两段组成:一段是从顶点vi到顶点vk的中间顶点序号不大于k-1的最短路径,另一段是从顶点vk到顶点vj的中间顶点序号不大于k-1的最短路径,路径长度应为这两段长度之和,用下面的公式计算:
。综合这两种情况有:
这样等于从顶点vi到顶点vj中间序号不大于n的最短路径长度,也就是所求的从顶点vi到顶点vj的最短路径。
有时,除了计算一个带权图中从任意一个顶点到其他顶点的最短路径长度,还需要确定相应的最短路径。为此,可以设置一个n*n的矩阵path,path[i,j]是由顶点vi到vj的最短路径上排在顶点vj前面那个顶点,即当k在Floyd算法中使得达到最小值,就置path[i,j]=k。如果没有最短路径,就将path[i,j]置为-1。
/* Floyd 算法用于求解图 G 中所有顶点对之间的最短路径。
*
* 参数说明:
* G:输入的图,使用邻接矩阵表示,G.VerticsNum() 返回图的顶点数,G.FirstEdge(v) 返回顶点 v 的第一条边,G.IsEdge(e) 判断边 e 是否存在,G.ToVertex(e) 返回边 e 的终点,G.Weight(e) 返回边 e 的权重
* D:输出的最短路径长度和前驱顶点矩阵,D[i][j].length 存储顶点 i 到 j 的最短路径长度,D[i][j].pre 存储顶点 i 到 j 最短路径上的前驱顶点
*/
void Floyd(Graph &G, Dist **&D) {
int i, j, v;
D = new Dist*[G.VerticsNum()]; // 为 D 数组申请空间,每个元素都指向一个新的 Dist 对象
for (int i = 0; i < G.VerticsNum(); i++); // 初始化 D 数组,除最后一个元素外的所有元素都指向一个随机的 Dist 对象
D[i] = new Dist[G.VerticsNum()];
for (int i = 0; i < G.VerticsNum(); i++) { // 初始化数组 D
for (int j = 0; j < G.VerticsNum(); j++) {
if (i == j) {
D[i][j].length = 0; // 顶点 i 到 j 的路径长度为 0
D[i][j].pre = i; // 顶点 i 到 j 最短路径上的前驱顶点是 i 本身
} else {
D[i][j].length = INFINITE; // 顶点 i 到 j 的路径长度初始化为无穷大
D[i][j].pre = -1; // 顶点 i 到 j 最短路径上的前驱顶点初始化为 -1
}
}
}
for (v = 0; v < G.VerticsNum(); v++) { // 将图 G 中的边权值赋给 D 数组
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = G.NextEdge(e)) {
D[v][G.ToVertex(e)].length = G.Weight(e); // 边 e 的权值赋给 D 数组对应位置的路径长度
D[v][G.ToVertex(e)].pre = v; // 顶点 v 是顶点 G.ToVertex(e) 的前驱顶点
}
}
for (v = 0; v < G.VerticsNum(); v++) { // 更新所有顶点对之间的最短路径
for (i = 0; i < G.VerticsNum(); i++) {
for (j = 0; j < G.VerticsNum(); j++) {
if (D[i][j].length > D[i][v].length + D[v][j].length) {
D[i][j].length = D[i][v].length + D[v][j].length; // 顶点 i 到 j 的路径长度经过顶点 v 而变短了,则更新路径长度
D[i][j].pre = D[v][j].pre; // 更新顶点 i 到 j 最短路径上的前驱顶点
}
}
}
}
}
4、最小生成树
图G的生成树是一棵包含G的所有顶点的树,树中所有权值总和表示代价,在G的所有生成树中,代价最小的生成树称为最小生成树(minimum-cost spanning tree, MST)。
构造生成树有很多算法。这里主要介绍Prim算法和Krustal算法,都是贪心算法,都利用了下面最小生成树的性质。
设G = <V, E>是一个连通的带权图,其中每条边(vi, vj)上带有权W(vi, vj)。集合U是顶点集V的一个非空真子集。构建生成树需要一条边连通顶点集合U和V-U。如果,其中
,
,且边(u,v)是符合条件的权值W(u,v)是最小的,那么一定存在一棵包含边(u,v)的的最小生成树。这条性质称为MST性质。
4.1 Prim算法
设G<V,E>是一个连通的带权图,其中V是顶点的集合,E是边的集合,TE为最小生成树的边的集合。则Prim算法通过以下步骤得到最小生成树:
(1)初始状态:U={u0},TE={}。其中u0是顶点集合V中的某一个顶点。
(2)在所有
的边
中找一条权值最小的边(u0,v0),将这条边加到集合TE中,同时将此边的另一个顶点v0并入U。这一步骤的作用是在边集E中找到一条两个顶点分别在U和V-U中的最小权值的边,并且将它并入TE中,并把这条边上不在U中的顶点加到U中。(3)如果U = V,则算法结束,否则重复步骤(2)。
算法结束时,TE中包含了G的n-1条边。经过上述步骤选取到的所有边恰好就构成了图G的一棵最小生成树。对于上图所示的带权图,按照Prim算法选取边的过程如下图所示。
//在Dist数组中找最小值
int minVertex(Graph &G, Dist* & D) {
int i,v;
for (i = 0; i < G.VerticesNum(); i++) {
if (G.Mark[i] == UNVISITED) {
v = i; //使v为随意一个未访问的顶点
break;
}
}
for (i = 0; i < G.VerticesNum(); i++) {
if (G.Mark[v] != UNVISITED && D[i] < D[v]) {
v = i; //保存当前发现的具有最小距离的点
}
return v; //返回当前最小顶点的索引
}
}
//Prim算法
void Prim(Graph &G, int s; Edge* &MST) { //s是开始顶点,数组MST用于保存最小生成树的边
int MSTtag = 0; //最小生成树的边计数
MST = new Edge[G.VerticsNum()-1]; //为数组MST申请空间
D = new Dist[G.VerticsNum()]; //为数组D申请空间
for (int i = 0; i < G.VerticsNum(); i++) { //初始化Mark数组、D数组
G.Mark[i] = UNVISTED; //标记所有顶点未访问
D[i].index = i; //存储顶点索引
D[i].length = INFINITE;
D[i].pre = s; //预先设置起点s的前驱为s
}
D[s].length = 0; //起点s的距离设置为0,表示已经访问过
G.Mark[s] = VISITED; //开始顶点标记设置为VISITED
int v = s; //当前考虑的未访问顶点
for (i = 0; i < VerticsNum() - 1; i++) {
if (D[i].length == INFINITY) return;
//因为v的加入,需要刷新与v相邻接的顶点D的值
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = G.NextEdge(e)) {
if (G.Mark[e] != VISTED && D[G.ToVertex(e)] > e.weight) {
D[G.ToVertex(e)].length = e.weight;
D[G.ToVertex(e)].pre = v; //设置新前驱为当前结点v
}
}
//找出下一个待访问的未访问结点
v = minVertex(G, D); //在D数组中找最小值记为v
G.Mark[v] = VISITED; //标记访问过
Edge edge(D[v].pre, D[v].index, D[v].length); //保存边
AddVertextoMag(edge, MST, MSTtag++); //将边edge加入到MST数组中
}
}
Prim算法非常类似于Dijkstra算法,但是Prim算法中的距离不需要累积,直接采用离集合最近的边距。Prim算法需要的时间复杂度也与Dijkstra算法相同。本算法直接通过比较D数组元素,确定代价最小的边就需要总时间O(n^2);取出权最小的边之后,修改D数组共需要时间O(e),因此共需要花费时间为O(n^2)。
4.2 Krustal算法
构造最小生成树的另一个常用算法是Krustal算法。Krustal算法使用的贪心准则是从剩下的边中选择不会产生环路且具有最小权值的边加入生成树的边集中。
给定含有n个顶点和e条边的无向连通带权图G<V,E>,Krustal算法构造思想是:首先将G中的n个顶点看成独立的n个连通分量,这时的状态是有n个顶点而无边的森林,可以记为T=<V,{}>。然后,在E中选择最小的边,如果该边依附于两个不同的连通分支,那么就将这条边加入到T中,否则舍去这条边而选择下一条权值最小的边。以此类推,直到T中所有顶点都在同一个连通分量为止,此时得到的图G就是一棵最小生成树。
下图展示了按照Krustal算法构造最小生成树的过程。
在Krustal算法中,把连通分量的顶点作为集合元素,采用并查集的Find操作确定两个关联顶点所属的连通分支,采用Union算法合并两个连通分量。
在Krustal算法中,需要按照边权值递增的顺序依次查看边,可以把边的权值组织优先队列,权值越小,优先级越高。用最小堆来实现这个优先队列。
void Krustal(Graph &G, Edge *&MST) {
ParTree<int> A(G.VertivsNum()); //等价类对象A,用于判断顶点之间的关系
MinHeap<Edge>H(G.EdgesNum()); //最小堆对象H,用于按权值排序边
MST = new Edge[G.VertivsNum() - 1]; //为MST数组申请空间
int MSTtag = 0;
bool heapEmpty;
//将图的所有边加入到最小堆中
for (int i = 0; i < G.VertivsNum(); i++)
for (Edge e = G.FirstEdge(i); G.IsEdge(e); e = G.NextEdge(e))
if (G.FromVertex() < G.ToVertex()); //对于无向图,防止重复插入边
H.Insert(e);
//开始时,等价类的个数等于顶点数
int EquNum = G.VertivsNum();
//当等价类的个数大于1时合并等价类
while (EquNum > 1)
heapEmpty = H.isEmpty();
//如果堆为空,则无法构建最小生成树
if (!heapEmpty)
Edge e = H.RemoveMin(); //获得权值最小的的边
//如果权值为无穷大则无法构建最小生成树
if (heapEmpty || e.weight == INFINITY) {
cout <<"不存在最小生成树"<< endl;
delete[] MST; //释放空间
MST = NULL; //MST赋为空
return;
}
//获取边的起点和终点
int from = G.FromVertex(e);
int to = G.ToVertex(e);
//如果边的两个顶点不在同一个等价类中,可以将边加入最小生成树
if (A.Different(from, to)) {
A.union(from, to); //将边的两个顶点所在的等价类合并为一个
AddEdgetoMST(e, MST, MSTtag++); //将边加入最小生成树中
EquNum--; //等价类的个数减1
}
}
Krustal算法的时间复杂度为O(eloge)。这个算法的时间复杂度主要取决于边数,因此Krustal算法适合构造稀疏图的最小生成树。
参考文献
[1]张铭,赵海燕,王腾蛟. 数据结构与算法——北京:高等教育出版社 2008
[2]2025王道数据结构考研复习指导