编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)

介绍

克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
基本思想:
按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
具体做法:
首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入森林中,并使森林不产生回路,直至森林变成一棵树为止。

图解说明

编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第1张图片
以上图G4为例,来对克鲁斯卡尔进行说明(假设:用数组R保存最小生成树结果)
(1)将边加入R中
的权值最小,因此将它加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第2张图片
(2)将边加入R中
上一步操作之后,边的权值最小,因此将它加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第3张图片
(3)将边加入R中
上一步操作之后,边的权值最小,因此将它加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第4张图片
(4)将边加入R中
上一步操作之后,边的权值最小,但会和已有的边构成回路;因此,跳过边。同理跳过边。将边加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第5张图片
(5)将边加入R中
上一步操作之后,边的权值最小,因此将它加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第6张图片
(6)将边加入R中
上一步操作之后,边的权值最小,但会和已有的边构成回路;因此,跳过边。同理跳过边。将边加入到最小生成树结果R中。
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第7张图片
此时,最小生成树构造完成!它包括的边的边依次为:

克鲁斯卡尔算法的重点问题:
问题1:对图中的所有边按照权值大小进行排序。
解决办法:采用排序算法进行排序即可

问题2:将边添加到最小生成树中时,怎么样判断是否形成回路。
解决办法:记录顶点在“最小生成树”中的终点,顶点的终点是“在最小生成树中与它连通的最大顶点”。然后,每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。
举例说明:
编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第8张图片
加入到最小生成树R中之后,这几条边的顶点就都有了终点:
1)C的终点是F
2)D的终点是F
3)E的终点是F
4)F的终点是F

关于终点的说明:
1)就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是“与它连通的最大顶点”。
2)因此,接下来,虽然是权值最小的边。但是C和E的终点都是F,即它们的终点相同,因此,将加入最小生成树,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。

代码实现(Java):



package com.zq.kruskal;

 

import java.util.Arrays;

 

public class KruskalCase {

  private int edgeNum;//边的个数

  private char[] vertexs;//顶点数组

  private int[][] matrix;//邻接矩阵

  

  //使用INF表示两个顶点不能连通

  private static final int INF=Integer.MAX_VALUE;

  

  public static void main(String[] args) {

     char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};

     //克鲁斯卡尔算法的邻接矩阵  

        int matrix[][] = {

        /*A*//*B*//*C*//*D*//*E*//*F*//*G*/

  /*A*/ {   0,  12, INF, INF, INF, 
16,  14},

  /*B*/ {  12,   0,  10, INF, INF,   7, INF},

  /*C*/ { INF,  10,   0,   3,   5,   6, INF},

  /*D*/ { INF, INF,  
3,   0,   4, INF, INF},

  /*E*/ { INF, INF,  
5,   4,   0,   2,   8},

  /*F*/ {  16,   7,   6, INF,  
2,   0,   9},

  /*G*/ {  14, INF, INF, INF,  
8,   9,   0}}; 

        //大家可以在去测试其它的邻接矩阵,结果都可以得到最小生成树.

        

        //创建KruskalCase 对象实例

        KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);

        //输出构建的

        kruskalCase.print();

        kruskalCase.kruskal();

        

  }

  

  public KruskalCase(char[] vertexs,int[][] matrix) {

     // TODO Auto-generated
constructor stub

     //初始化顶点数和边的个数

     int vlen=vertexs.length;

     //初始化顶点

     this.vertexs=new char[vlen];

     for(int i=0;i<vertexs.length;i++) {

       this.vertexs[i]=vertexs[i];

     }

     //初始化边

     this.matrix=new int[vlen][vlen];

     for(int i=0;i<vlen;i++) {

       for(int j=0;j<vlen;j++) {

         this.matrix[i][j]=matrix[i][j];

       }

     }

     //统计边的条数

     for(int i=0;i<vlen;i++) {

       for(int j=i+1;j<vlen;j++) {

          if (this.matrix[i][j]!=INF) {

            edgeNum++;

          }

       }

     }

     

  }

  

  public void kruskal() {

     int index=0;//表示最后结果数组的索引

     int[] ends=new int[edgeNum];//用于保存“已有最小生成树”中的每个顶点在最小生成树中的终点

     

     //创建结果数组,保存最后的最小生成树

     EData[] rets=new EData[edgeNum];

     

     //获取图中所有边的集合

     EData[] edges=getEdges();

     System.out.println("图的边的集合="+Arrays.toString(edges)+"共"+edges.length);

     

     //按照边的权值大小进行排序(从小到大)

     sortEdges(edges);

     

     //遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否形成了回路,如果没有,就加入rets,否则不能加入

     for(int i=0;i<edgeNum;i++) {

       //获取到第i条边的第一个顶点

       int p1=getPosition(edges[i].start);

       //获取第i条边的第二个顶点

       int p2=getPosition(edges[i].end);

       

       //获取p1这个顶点在已有最小生成树中的终点

       int m=getEnd(ends, p1);

       //获取p2这个顶点在已有最小生成树中的终点

       int n=getEnd(ends, p2);

       if (m!=n) {

          ends[m]=n;

          rets[index++]=edges[i];

       }

     }

     //统计并打印”最小生成树“

     System.out.println("最小生成树为");

     for(int i=0;i<index;i++) {

       System.out.println(rets[i]);

     }

  }

  

  //打印连接矩阵

  public void print() {

     for(int i=0;i<vertexs.length;i++) {

       for(int j=0;j<vertexs.length;j++) {

          System.out.printf("%12d", matrix[i][j]);

       }

       System.out.println();

     }

 

  }

  

  

  //对边进行排序

  private void sortEdges(EData[] edges) {

     for(int i=0;i<edges.length-1;i++) {

       for(int j=0;j<edges.length-i-1;j++) {

          if (edges[j].weight>edges[j+1].weight) {

            EData tmp=edges[j];

            edges[j]=edges[j+1];

            edges[j+1]=tmp;

          }

          

       }

     }

  }

  

  //返回ch顶点对应的下标,如果找不到,返回-1

  private int getPosition(char ch) {

     for(int i=0;i<vertexs.length;i++) {

       if (vertexs[i]==ch) {

          return i;

       }

     }

     return -1;

  }

  

  //获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同

  private int getEnd(int[] ends,int i) {

     while(ends[i]!=0) {

       i=ends[i];

     }

     return i;

  }

  

  //获取图中边,放入EData数组中

  private EData[] getEdges(){

     int index=0;

     EData[] edges=new EData[edgeNum];

     for(int i=0;i<vertexs.length;i++){

       for(int j=i+1;j<vertexs.length;j++) {

          if (matrix[i][j]!=INF) {

            edges[index++]=new EData(vertexs[i], vertexs[j], matrix[i][j]);

          }

       }

     }

     return edges;

  }

  

  

 

  //创建一个类EData,它的对象实例就表示一条边

  class EData{

     char start;//边的一个点

     char end;//边的另一个点

     int weight;//边的权值

     public EData(char start,char end,int weight) {

       // TODO Auto-generated
constructor stub

       this.start=start;

       this.end=end;

       this.weight=weight;

     }

     @Override

     public String toString() {

       // TODO Auto-generated
method stub

       return "EData[<"+start+","+end+">="+weight+"]";

     }

  }

}

 

结果:

编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第9张图片

编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树)_第10张图片
参考文献:
1.韩顺平-图解Java数据结构和算法

你可能感兴趣的:(编程中十大常用算法:(七)克鲁斯卡尔算法(最小生成树))