基于C#实现Kruskal算法

这篇我们看看第二种生成树的 Kruskal 算法,这个算法的魅力在于我们可以打一下算法和数据结构的组合拳,很有意思的。

一、思想

若存在 M={0,1,2,3,4,5}这样 6 个节点,我们知道 Prim 算法构建生成树是从"顶点"这个角度来思考的,然后采用"贪心思想"来一步步扩大化,最后形成整体最优解,而 Kruskal 算法有点意思,它是站在"边"这个角度在思考的,首先我有两个集合。

1.1、顶点集合(vertexs)

比如 M 集合中的每个元素都可以认为是一个独根树(是不是想到了并查集?)。

1.2、边集合(edges)

对图中的每条边按照权值大小进行排序。(是不是想到了优先队列?)

首先:我们从 edges 中选出权值最小的一条边来作为生成树的一条边,然后将该边的两个顶点合并为一个新的树。

然后:我们继续从 edges 中选出次小的边作为生成树的第二条边,但是前提就是边的两个顶点一定是属于两个集合中,如果不是则剔除该边继续选下一条次小边。

最后:经过反复操作,当我们发现 n 个顶点的图中生成树已经有 n-1 边的时候,此时生成树构建完毕。

从图中我们还是很清楚的看到 Kruskal 算法构建生成树的详细过程,同时我们也看到了"并查集"和"优先队列"这两个神器来加速我们的生成树构建。

二、构建

2.1、Build 方法

这里我灌的是一些测试数据,同时在矩阵构建完毕后,将顶点信息放入并查集,同时将边的信息放入优先队列,方便我们在做生成树的时候秒杀。

csharp 复制代码
 #region 矩阵的构建
 /// <summary>
 /// 矩阵的构建
 /// </summary>
 public void Build()
 {
     //顶点数
     graph.vertexsNum = 6;

     //边数
     graph.edgesNum = 8;

     graph.vertexs = new int[graph.vertexsNum];

     graph.edges = new int[graph.vertexsNum, graph.vertexsNum];

     //构建二维数组
     for (int i = 0; i < graph.vertexsNum; i++)
     {
         //顶点
         graph.vertexs[i] = i;

         for (int j = 0; j < graph.vertexsNum; j++)
         {
             graph.edges[i, j] = int.MaxValue;
         }
     }

     graph.edges[0, 1] = graph.edges[1, 0] = 80;
     graph.edges[0, 3] = graph.edges[3, 0] = 100;
     graph.edges[0, 5] = graph.edges[5, 0] = 20;
     graph.edges[1, 2] = graph.edges[2, 1] = 90;
     graph.edges[2, 5] = graph.edges[5, 2] = 70;
     graph.edges[4, 5] = graph.edges[5, 4] = 40;
     graph.edges[3, 4] = graph.edges[4, 3] = 60;
     graph.edges[2, 3] = graph.edges[3, 2] = 10;

     //优先队列,存放树中的边
     queue = new PriorityQueue<Edge>();

     //并查集
     set = new DisjointSet<int>(graph.vertexs);

     //将对角线读入到优先队列
     for (int i = 0; i < graph.vertexsNum; i++)
     {
         for (int j = i; j < graph.vertexsNum; j++)
         {
             //说明该边有权重
             if (graph.edges[i, j] != int.MaxValue)
             {
                 queue.Eequeue(new Edge()
                 {
                     startEdge = i,
                     endEdge = j,
                     weight = graph.edges[i, j]
                 }, graph.edges[i, j]);
             }
         }
     }
 }
 #endregion

2.2、Kruskal 算法

并查集,优先队列都有数据了,下面我们只要出队操作就行了,如果边的顶点不在一个集合中,我们将其收集作为最小生成树的一条边,按着这样的方式,最终生成树构建完毕。

csharp 复制代码
 #region Kruskal算法
 /// <summary>
 /// Kruskal算法
 /// </summary>
 public List<Edge> Kruskal()
 {
     //最后收集到的最小生成树的边
     List<Edge> list = new List<Edge>();

     //循环队列
     while (queue.Count() > 0)
     {
         var edge = queue.Dequeue();

         //如果该两点是同一个集合,则剔除该集合
         if (set.IsSameSet(edge.t.startEdge, edge.t.endEdge))
             continue;

         list.Add(edge.t);

         //然后将startEdge 和 endEdge Union起来,表示一个集合
         set.Union(edge.t.startEdge, edge.t.endEdge);

         //如果n个节点有n-1边的时候,此时生成树已经构建完毕,提前退出
         if (list.Count == graph.vertexsNum - 1)
             break;
     }

     return list;
 }
 #endregion

最后是总的代码:

csharp 复制代码
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Diagnostics;
 using System.Threading;
 using System.IO;
 using System.Threading.Tasks;
 
 namespace ConsoleApplication2
 {
     public class Program
     {
         public static void Main()
         {
             MatrixGraph graph = new MatrixGraph();
 
             graph.Build();
 
             var edges = graph.Kruskal();
 
             foreach (var edge in edges)
             {
                 Console.WriteLine("({0},{1})({2})", edge.startEdge, edge.endEdge, edge.weight);
             }
 
             Console.Read();
         }
     }
 
     #region 定义矩阵节点
     /// <summary>
     /// 定义矩阵节点
     /// </summary>
     public class MatrixGraph
     {
         Graph graph = new Graph();
 
         PriorityQueue<Edge> queue;
 
         DisjointSet<int> set;
 
         public class Graph
         {
             /// <summary>
             /// 顶点信息
             /// </summary>
             public int[] vertexs;
 
             /// <summary>
             /// 边的条数
             /// </summary>
             public int[,] edges;
 
             /// <summary>
             /// 顶点个数
             /// </summary>
             public int vertexsNum;
 
             /// <summary>
             /// 边的个数
             /// </summary>
             public int edgesNum;
         }
 
         #region 矩阵的构建
         /// <summary>
         /// 矩阵的构建
         /// </summary>
         public void Build()
         {
             //顶点数
             graph.vertexsNum = 6;
 
             //边数
             graph.edgesNum = 8;
 
             graph.vertexs = new int[graph.vertexsNum];
 
             graph.edges = new int[graph.vertexsNum, graph.vertexsNum];
 
             //构建二维数组
             for (int i = 0; i < graph.vertexsNum; i++)
             {
                 //顶点
                 graph.vertexs[i] = i;
 
                 for (int j = 0; j < graph.vertexsNum; j++)
                 {
                     graph.edges[i, j] = int.MaxValue;
                 }
             }
 
             graph.edges[0, 1] = graph.edges[1, 0] = 80;
             graph.edges[0, 3] = graph.edges[3, 0] = 100;
             graph.edges[0, 5] = graph.edges[5, 0] = 20;
             graph.edges[1, 2] = graph.edges[2, 1] = 90;
             graph.edges[2, 5] = graph.edges[5, 2] = 70;
             graph.edges[4, 5] = graph.edges[5, 4] = 40;
             graph.edges[3, 4] = graph.edges[4, 3] = 60;
             graph.edges[2, 3] = graph.edges[3, 2] = 10;

             //优先队列,存放树中的边
             queue = new PriorityQueue<Edge>();
 
             //并查集
             set = new DisjointSet<int>(graph.vertexs);
 
             //将对角线读入到优先队列
             for (int i = 0; i < graph.vertexsNum; i++)
             {
                 for (int j = i; j < graph.vertexsNum; j++)
                 {
                     //说明该边有权重
                     if (graph.edges[i, j] != int.MaxValue)
                     {
                         queue.Eequeue(new Edge()
                         {
                             startEdge = i,
                             endEdge = j,
                             weight = graph.edges[i, j]
                         }, graph.edges[i, j]);
                     }
                 }
             }
         }
         #endregion
 
         #region 边的信息
         /// <summary>
         /// 边的信息
         /// </summary>
         public class Edge
         {
             //开始边
             public int startEdge;
 
             //结束边
             public int endEdge;
 
             //权重
             public int weight;
         }
         #endregion
 
         #region Kruskal算法
         /// <summary>
         /// Kruskal算法
         /// </summary>
         public List<Edge> Kruskal()
         {
             //最后收集到的最小生成树的边
             List<Edge> list = new List<Edge>();
 
             //循环队列
             while (queue.Count() > 0)
             {
                 var edge = queue.Dequeue();
 
                 //如果该两点是同一个集合,则剔除该集合
                 if (set.IsSameSet(edge.t.startEdge, edge.t.endEdge))
                     continue;
 
                 list.Add(edge.t);
 
                 //然后将startEdge 和 endEdge Union起来,表示一个集合
                 set.Union(edge.t.startEdge, edge.t.endEdge);
 
                 //如果n个节点有n-1边的时候,此时生成树已经构建完毕,提前退出
                 if (list.Count == graph.vertexsNum - 1)
                     break;
             }
 
             return list;
         }
         #endregion
     }
     #endregion
 }

并查集:

csharp 复制代码
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
 namespace ConsoleApplication2
 {
     /// <summary>
     /// 并查集
     /// </summary>
     public class DisjointSet<T> where T : IComparable
     {
         #region 树节点
         /// <summary>
         /// 树节点
         /// </summary>
         public class Node
         {
             /// <summary>
             /// 父节点
             /// </summary>
             public T parent;
 
             /// <summary>
             /// 节点的秩
             /// </summary>
             public int rank;
         }
         #endregion
 
         Dictionary<T, Node> dic = new Dictionary<T, Node>();
 
         public DisjointSet(T[] c)
         {
             Init(c);
         }
 
         #region 做单一集合的初始化操作
         /// <summary>
         /// 做单一集合的初始化操作
         /// </summary>
         public void Init(T[] c)
         {
             //默认的不想交集合的父节点指向自己
             for (int i = 0; i < c.Length; i++)
             {
                 dic.Add(c[i], new Node()
                 {
                     parent = c[i],
                     rank = 0
                 });
             }
         }
         #endregion
 
         #region 判断两元素是否属于同一个集合
         /// <summary>
         /// 判断两元素是否属于同一个集合
         /// </summary>
         /// <param name="root1"></param>
         /// <param name="root2"></param>
         /// <returns></returns>
         public bool IsSameSet(T root1, T root2)
         {
             return Find(root1).CompareTo(Find(root2)) == 0;
         }
         #endregion
 
         #region  查找x所属的集合
         /// <summary>
         /// 查找x所属的集合
         /// </summary>
         /// <param name="x"></param>
         /// <returns></returns>
         public T Find(T x)
         {
             //如果相等,则说明已经到根节点了,返回根节点元素
             if (dic[x].parent.CompareTo(x) == 0)
                 return x;
 
             //路径压缩(回溯的时候赋值,最终的值就是上面返回的"x",也就是一条路径上全部被修改了)
             return dic[x].parent = Find(dic[x].parent);
         }
         #endregion
 
         #region 合并两个不相交集合
         /// <summary>
         /// 合并两个不相交集合
         /// </summary>
         /// <param name="root1"></param>
         /// <param name="root2"></param>
         /// <returns></returns>
         public void Union(T root1, T root2)
         {
             T x1 = Find(root1);
             T y1 = Find(root2);
 
             //如果根节点相同则说明是同一个集合
             if (x1.CompareTo(y1) == 0)
                 return;
 
             //说明左集合的深度 < 右集合
             if (dic[x1].rank < dic[y1].rank)
             {
                 //将左集合指向右集合
                 dic[x1].parent = y1;
             }
             else
             {
                 //如果 秩 相等,则将 y1 并入到 x1 中,并将x1++
                 if (dic[x1].rank == dic[y1].rank)
                     dic[x1].rank++;
 
                 dic[y1].parent = x1;
             }
         }
         #endregion
     }
 }

优先队列:

csharp 复制代码
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Diagnostics;
 using System.Threading;
 using System.IO;
 
 namespace ConsoleApplication2
 {
     public class PriorityQueue<T> where T : class
     {
         /// <summary>
         /// 定义一个数组来存放节点
         /// </summary>
         private List<HeapNode> nodeList = new List<HeapNode>();
 
         #region 堆节点定义
         /// <summary>
         /// 堆节点定义
         /// </summary>
         public class HeapNode
         {
             /// <summary>
             /// 实体数据
             /// </summary>
             public T t { get; set; }
 
             /// <summary>
             /// 优先级别 1-10个级别 (优先级别递增)
             /// </summary>
             public int level { get; set; }
 
             public HeapNode(T t, int level)
             {
                 this.t = t;
                 this.level = level;
             }
 
             public HeapNode() { }
         }
         #endregion
 
         #region  添加操作
         /// <summary>
         /// 添加操作
         /// </summary>
         public void Eequeue(T t, int level = 1)
         {
             //将当前节点追加到堆尾
             nodeList.Add(new HeapNode(t, level));
 
             //如果只有一个节点,则不需要进行筛操作
             if (nodeList.Count == 1)
                 return;
 
             //获取最后一个非叶子节点
             int parent = nodeList.Count / 2 - 1;
 
             //堆调整
             UpHeapAdjust(nodeList, parent);
         }
         #endregion
 
         #region 对堆进行上滤操作,使得满足堆性质
         /// <summary>
         /// 对堆进行上滤操作,使得满足堆性质
         /// </summary>
         /// <param name="nodeList"></param>
         /// <param name="index">非叶子节点的之后指针(这里要注意:我们
         /// 的筛操作时针对非叶节点的)
         /// </param>
         public void UpHeapAdjust(List<HeapNode> nodeList, int parent)
         {
             while (parent >= 0)
             {
                 //当前index节点的左孩子
                 var left = 2 * parent + 1;
 
                 //当前index节点的右孩子
                 var right = left + 1;
 
                 //parent子节点中最大的孩子节点,方便于parent进行比较
                 //默认为left节点
                 var min = left;
 
                 //判断当前节点是否有右孩子
                 if (right < nodeList.Count)
                 {
                     //判断parent要比较的最大子节点
                     min = nodeList[left].level < nodeList[right].level ? left : right;
                 }
 
                 //如果parent节点大于它的某个子节点的话,此时筛操作
                 if (nodeList[parent].level > nodeList[min].level)
                 {
                     //子节点和父节点进行交换操作
                     var temp = nodeList[parent];
                     nodeList[parent] = nodeList[min];
                     nodeList[min] = temp;
 
                     //继续进行更上一层的过滤
                     parent = (int)Math.Ceiling(parent / 2d) - 1;
                 }
                 else
                 {
                     break;
                 }
             }
         }
         #endregion
 
         #region 优先队列的出队操作
         /// <summary>
         /// 优先队列的出队操作
         /// </summary>
         /// <returns></returns>
         public HeapNode Dequeue()
         {
             if (nodeList.Count == 0)
                 return null;
 
             //出队列操作,弹出数据头元素
             var pop = nodeList[0];
 
             //用尾元素填充头元素
             nodeList[0] = nodeList[nodeList.Count - 1];
 
             //删除尾节点
             nodeList.RemoveAt(nodeList.Count - 1);
 
             //然后从根节点下滤堆
             DownHeapAdjust(nodeList, 0);
 
             return pop;
         }
         #endregion
 
         #region  对堆进行下滤操作,使得满足堆性质
         /// <summary>
         /// 对堆进行下滤操作,使得满足堆性质
         /// </summary>
         /// <param name="nodeList"></param>
         /// <param name="index">非叶子节点的之后指针(这里要注意:我们
         /// 的筛操作时针对非叶节点的)
         /// </param>
         public void DownHeapAdjust(List<HeapNode> nodeList, int parent)
         {
             while (2 * parent + 1 < nodeList.Count)
             {
                 //当前index节点的左孩子
                 var left = 2 * parent + 1;
 
                 //当前index节点的右孩子
                 var right = left + 1;
 
                 //parent子节点中最大的孩子节点,方便于parent进行比较
                 //默认为left节点
                 var min = left;
 
                 //判断当前节点是否有右孩子
                 if (right < nodeList.Count)
                 {
                     //判断parent要比较的最大子节点
                     min = nodeList[left].level < nodeList[right].level ? left : right;
                 }
 
                 //如果parent节点小于它的某个子节点的话,此时筛操作
                 if (nodeList[parent].level > nodeList[min].level)
                 {
                     //子节点和父节点进行交换操作
                     var temp = nodeList[parent];
                     nodeList[parent] = nodeList[min];
                     nodeList[min] = temp;
 
                     //继续进行更下一层的过滤
                     parent = min;
                 }
                 else
                 {
                     break;
                 }
             }
         }
         #endregion
 
         #region 获取元素并下降到指定的level级别
         /// <summary>
         /// 获取元素并下降到指定的level级别
         /// </summary>
         /// <returns></returns>
         public HeapNode GetAndDownPriority(int level)
         {
             if (nodeList.Count == 0)
                 return null;
 
             //获取头元素
             var pop = nodeList[0];
 
             //设置指定优先级(如果为 MinValue 则为 -- 操作)
             nodeList[0].level = level == int.MinValue ? --nodeList[0].level : level;
 
             //下滤堆
             DownHeapAdjust(nodeList, 0);
 
             return nodeList[0];
         }
         #endregion
 
         #region 获取元素并下降优先级
         /// <summary>
         /// 获取元素并下降优先级
         /// </summary>
         /// <returns></returns>
         public HeapNode GetAndDownPriority()
         {
             //下降一个优先级
             return GetAndDownPriority(int.MinValue);
         }
         #endregion
 
         #region 返回当前优先队列中的元素个数
         /// <summary>
         /// 返回当前优先队列中的元素个数
         /// </summary>
         /// <returns></returns>
         public int Count()
         {
             return nodeList.Count;
         }
         #endregion
     }
 }
相关推荐
希望有朝一日能如愿以偿26 分钟前
力扣题解(飞机座位分配概率)
算法·leetcode·职场和发展
小小娥子29 分钟前
Redis的基础认识与在ubuntu上的安装教程
java·数据库·redis·缓存
DieSnowK31 分钟前
[Redis][集群][下]详细讲解
数据库·redis·分布式·缓存·集群·高可用·新手向
丶Darling.38 分钟前
代码随想录 | Day26 | 二叉树:二叉搜索树中的插入操作&&删除二叉搜索树中的节点&&修剪二叉搜索树
开发语言·数据结构·c++·笔记·学习·算法
JustCouvrir41 分钟前
代码随想录算法训练营Day15
算法
小小工匠1 小时前
加密与安全_HOTP一次性密码生成算法
算法·安全·htop·一次性密码
中文英文-我选中文1 小时前
排序算法的理解
算法·排序算法
-XWB-1 小时前
【MySQL】数据目录迁移
数据库·mysql
老华带你飞1 小时前
公寓管理系统|SprinBoot+vue夕阳红公寓管理系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot·课程设计
我明天再来学Web渗透1 小时前
【hot100-java】【二叉树的层序遍历】
java·开发语言·数据库·sql·算法·排序算法