数据结构 最小生成树 Kruskal算法

时间:2022-12-02 11:37:45

数据结构 最小生成树 Kruskal算法

无向连通图的生成树不是唯一的。连通图的一次遍历所经过的边的集合及图中所有顶点的集合就构成了该图的一棵生成树,对连通图的不同遍历,就可能得到不同的生成树,那么,它的所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小生成树,简称为最小生成树

最小生成树:
具有最小权重且连接到所有结点的树

对于带权无向图G=(V, E),V表示图中的结点,E表示图中的边,最小生成树为集合T, T是以最小代价连接V中所有顶点所用边E的最小集合(就是联通图G中所有结点所需要的边长度总和最小)。 集合T中的边能够形成一颗树,因为每个节点(除根节点)都能向上找到它的一个父节点,这些边加上V就构成图G的一颗最小生成树

安全边:
假设A是某棵最小生成树的子集,下一步我们需要做的就是找出一条边(u,v),将其加入到A中,使得A∪{(u,v)}也是某棵最小生成树的子集,我们称(u,v)为安全边
切割(S,V-S):
—对集合V的一个划分,将集合V划分为两个部分S和V-S
横跨切割:
—若边(u,v)的一个节点属于S另一个节点属于V-S,则称(u,v)横跨切割
尊重集合A:
如果A中不存在横跨切割的边,则称该集合尊重集合A

横跨尊重集合A的切割(S,V-S)的一条轻量级边(u,v)为A的一条安全边

Prim算法和Kruskal算法在实现方面的区别:
1、Kruskal算法在生成最小生成树的过程中产生的是森林,
Prim算法在执行过程中始终都是一棵树;
2、Kruskal和Prim实现上的最大区别:
Kruskal不需要搜索每个顶点的邻接节点,
Prim图构建时需要利用邻接链表进行构建,Kruskal不用!
3、Kruskal算法在效率上要比Prim算法快,
Kruskal只需要对权重边做一次排序
而Prim算法则需要做多次排序

Prim算法是依赖于点的算法:
它的基本原理是从当前点寻找一个离自己(集合)最近的点然后把这个点拉到自己家来(距离设为0),同时输出一条边,并且刷新到其他点的路径长度。俗称,刷表。
根据Prim算法的特性可以得知,它很适合于点密集的图。对Prim算法通常采用邻接矩阵的储存结构。这种储存方法空间复杂度N^2,时间复杂度N^2。
对于稍微稀疏一点的图,其实我们更适合采用邻接表的储存方式,可以节省空间,并在一定条件下节省时间。

Kruskal算法是依赖边的算法:
基本原理是将边集数组排序,然后通过维护一个并查集来分清楚并进来的点和没并进来的点,依次按从小到大的顺序遍历边集数组,如果这条边对应的两个顶点不在一个集合内,就输出这条边,并合并这两个点。

根据Kruskal算法的特性可得,在边越少的情况下,Kruskal算法相对Prim算法的优势就越大。同时,因为边集数组便于维护,所以Kruskal在数据维护方面也较为简单,不像邻接表那样复杂。Kruskal算法的速度与点数无关,对于稀疏图可以采用Kruskal算法。

Kruskal算法(克鲁斯卡尔算法)

算法原理:
首先将每个顶点放入其自身的数据集合中。然后,按照权值的升序来选择边。当选择每条边时,判断定义边的顶点是否在不同的数据集中。如果是,将此边插入最小生成树的集合中,同时,将集合中包含每个顶点的联合体取出,如果不是,就移动到下一条边。重复这个过程直到所有的边都探查过

Kruskal在找最小生成树结点之前,需要对所有权重边做从小到大排序。将排序好的权重边依次加入到最小生成树中,如果加入时产生回路就跳过这条边,加入下一条边。当所有结点都加入到最小生成树中之后,就找出了最小生成树

算法描述:
1、新建图G,把图G中所有的边按照权值从小到大排序。(一般使用优先队列)
2、取出最小的边,如果这条边连接的两个节点于图G中不在同一个连通分量中,则添加这条边到图G中.(使用并查集)
3、重复步骤2,直至图G中所有的节点都在同一个连通分量中

性能分析
Kruskal算法,基本上取决于优先队列的选择,以及并查集的实现。
算法效率为 O(ElogE) E为图中的边数

时间复杂度:O(Elog2E) E为图中的边数
//对优先队列和并查集 //查找算法之顺序、二分、二叉搜索树、红黑树 和并查集

import java.io.IOException;
import java.util.Scanner;

public class Kruskal {
    private int mEdgNum;        // 边的数量
    private char[] mVexs;       // 顶点集合
    private int[][] mMatrix;    // 邻接矩阵
    private static final int INF = 100;//Integer.MAX_VALUE; // 最大值

    //创建图(用已提供的矩阵)
    public Kruskal(char[] vexs, int[][] matrix) {// vexs-- 顶点数组 ,matrix-- 矩阵(数据) 
        // 初始化"顶点数"和"边数"
        int vlen = vexs.length;
        // 初始化"顶点"
        mVexs = new char[vlen];
        for (int i = 0; i < mVexs.length; i++)
            mVexs[i] = vexs[i];
        // 初始化"边"
        mMatrix = new int[vlen][vlen];
        for (int i = 0; i < vlen; i++)
            for (int j = 0; j < vlen; j++)
                mMatrix[i][j] = matrix[i][j];
        // 统计"边"
        mEdgNum = 0;
        for (int i = 0; i < vlen; i++)
            for (int j = i+1; j < vlen; j++)
                if (mMatrix[i][j]!=INF)
                    mEdgNum++;
    }

    //返回ch位置
    private int getPosition(char ch) {
        for(int i=0; i<mVexs.length; i++)
            if(mVexs[i]==ch)
                return i;
        return -1;
    }

    // 返回顶点v的第一个邻接顶点的索引,失败则返回-1
    private int firstVertex(int v) {
        if (v<0 || v>(mVexs.length-1))
            return -1;
        for (int i = 0; i < mVexs.length; i++)
            if (mMatrix[v][i]!=0 && mMatrix[v][i]!=INF)
                return i;
        return -1;
    }

    //打印邻接顶点的索引、权重
    private void nextVertexs() {
        for(int v = 0; v < mVexs.length; v++){
            System.out.print("\n结点"+v);
            for (int i = 0; i < mVexs.length; i++) {
               if (mMatrix[v][i]!=0 && mMatrix[v][i]!=INF)
                  System.out.print("邻接点"+i+"(权重"+mMatrix[v][i]+")");
            }
        } 
    }

    //打印矩阵队列图
    public void print() {
        int i, j, sum;
        System.out.printf("结点表:");
        for (i=0; i < mVexs.length; i++) { 
            sum = 0;
            for (j=0; j < mVexs.length; j++) { 
               if (mMatrix[i][j]!=0 && mMatrix[i][j]!=INF) {
                  sum++;
               }
            }
            System.out.print("\n结点"+i+", 标识"+mVexs[i]+", 共有邻接点"+sum);
        }

        System.out.print("\n\n");
        System.out.printf("邻接矩阵:\n");
        for (i = 0; i < mVexs.length; i++) {
            for (j = 0; j < mVexs.length; j++)
                System.out.printf("%3d ", mMatrix[i][j]);
            System.out.printf("\n");
        }

        nextVertexs();
    }

    /* * 克鲁斯卡尔(Kruskal)最小生成树 */
    public void kruskal() {
        int index = 0;                      // rets数组的索引
        int[] vends = new int[mEdgNum];     // 用于保存"已有最小生成树"中每个顶点在该最小树中的终点。
        EData[] rets = new EData[mEdgNum];  // 结果数组,保存kruskal最小生成树的边
        EData[] edges;                      // 图对应的所有边

        edges = getEdges(); // 获取"图中所有的边"
        sortEdges(edges, mEdgNum); // 将边按照"权"的大小进行排序(从小到大)

        for (int i=0; i<mEdgNum; i++) {
            int p1 = getPosition(edges[i].start);      // 获取第i条边的"起点"的序号
            int p2 = getPosition(edges[i].end);        // 获取第i条边的"终点"的序号

            int m = getEnd(vends, p1);                 // 获取p1在"已有的最小生成树"中的终点
            int n = getEnd(vends, p2);                 // 获取p2在"已有的最小生成树"中的终点
            // 如果m!=n,意味着"边i"与"已经添加到最小生成树中的顶点"没有形成环路
            if (m != n) {
                vends[m] = n;                       // 设置m在"已有的最小生成树"中的终点为n
                rets[index++] = edges[i];           // 保存结果
            }
        }

        // 统计并打印"kruskal最小生成树"的信息
        int length = 0;
        for (int i = 0; i < index; i++)
            length += rets[i].weight;

        System.out.printf("\nKruskal=%d: ", length);
        for (int i = 0; i < index; i++)
            System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);
        System.out.printf("\n");
    }

    //获取图中的边
    private EData[] getEdges() {
        int index=0;
        EData[] edges;
        edges = new EData[mEdgNum];
        for (int i=0; i < mVexs.length; i++) {
            for (int j=i+1; j < mVexs.length; j++) {
                if (mMatrix[i][j]!=INF) {
                    edges[index++] = new EData(mVexs[i], mVexs[j], mMatrix[i][j]);
                }
            }
        }
        return edges;
    }

    //对边按照权值大小进行排序(由小到大)
    private void sortEdges(EData[] edges, int elen) {
        for (int i=0; i<elen; i++) {
            for (int j=i+1; j<elen; j++) {
                if (edges[i].weight > edges[j].weight) {// 交换"边i"和"边j"
                    EData tmp = edges[i];
                    edges[i] = edges[j];
                    edges[j] = tmp;
                }
            }
        }
    }

    //获取i的终点
    private int getEnd(int[] vends, int i) {
        while (vends[i] != 0)
            i = vends[i];
        return i;
    }

    // 边的结构体
    private static class EData {
        char start; // 边的起点
        char end;   // 边的终点
        int weight; // 边的权重
        public EData(char start, char end, int weight) {
            this.start = start;
            this.end = end;
            this.weight = weight;
        }
    };


    public static void main(String[] args) {
        char[] vexs = {'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}};
        Kruskal pG;
        pG = new Kruskal(vexs, matrix);

        pG.print();   // 输出图的有关信息

        pG.kruskal();   // Kruskal算法生成最小生成树
    }
}

程序输出:

结点表:
结点0, 标识A, 共有邻接点3
结点1, 标识B, 共有邻接点3
结点2, 标识C, 共有邻接点4
结点3, 标识D, 共有邻接点2
结点4, 标识E, 共有邻接点4
结点5, 标识F, 共有邻接点5
结点6, 标识G, 共有邻接点3

邻接矩阵:
0 12 100 100 100 16 14
12 0 10 100 100 7 100
100 10 0 3 5 6 100
100 100 3 0 4 100 100
100 100 5 4 0 2 8
16 7 6 100 2 0 9
14 100 100 100 8 9 0

结点0邻接点1(权重12)邻接点5(权重16)邻接点6(权重14)
结点1邻接点0(权重12)邻接点2(权重10)邻接点5(权重7)
结点2邻接点1(权重10)邻接点3(权重3)邻接点4(权重5)邻接点5(权重6)
结点3邻接点2(权重3)邻接点4(权重4)
结点4邻接点2(权重5)邻接点3(权重4)邻接点5(权重2)邻接点6(权重8)
结点5邻接点0(权重16)邻接点1(权重7)邻接点2(权重6)邻接点4(权重2)邻接点6(权重9)
结点6邻接点0(权重14)邻接点4(权重8)邻接点5(权重9)

Kruskal=36: (E,F) (C,D) (D,E) (B,F) (E,G) (A,B)