图的最小生成树算法(Prim和Kruskal)

图的邻接矩阵表示法可参考:https://www.jianshu.com/p/9f27288f6749
测试图如图所示:

测试图.png

普里姆(Prim)算法

思想:先选取一个顶点加入最小生成树,再选取与该顶点相连的边中的最小权值对应的顶点加入生成树,将这两个顶点作为一棵新的最小生成树,继续判断与该树相连的边的最小权值对应的顶点,并将其加入最小生成树,直到所有顶点均加入生成树为止。

    //最小生成树--Prim算法
    //每次都是从lowcost数组中选择权值最小的边加入生成树
    public void MiniSpanTree_Prim(MyGraph graph) {
        int min, i, j, k;
        //保存相关顶点下标
        int[] adjvex = new int[graph.getNumOfVertex()];
        //保存相关顶点间边的权值
        int[] lowcost = new int[graph.getNumOfVertex()];
        /**
         * lowcost值为0时表示此下标的顶点已经加入最小生成树,也就是将v0将入生成树
         * 因为该程序默认从0号下标顶点开始建立最小生成树,所以将lowcost初始化为0
         * 但最小生成树的建立并不一定需要从0号下标顶点开始
        **/
        lowcost[0] = 0;
        //初始化第一个顶点的下标为0
        adjvex[0] = 0;
        for(i = 1; i < graph.getNumOfVertex(); i++) {
            //将与v0顶点有边的权值存入数组
            lowcost[i] = edges[0][i];
            //初始化都为v0的下标
            adjvex[i] = 0;
        }
        for(i = 1; i < graph.getNumOfVertex(); i++) {
            //初始化最小权值,一般设置为一个不可能的大数字,此处是65535
            min = INF;
            //j用来作为顶点下标的循环变量;k用来存储最小权值顶点的下标
            j = 1; k = 0;
            while(j < graph.getNumOfVertex()) {
                //如果权值不为0且权值小于min
                if (lowcost[j] != 0 && lowcost[j] < min) {
                    //让当前权值称为最小值
                    min = lowcost[j];
                    //将当前最小值的下标存入k
                    k = j;
                }
                j++;
            }
            //输出当前顶点边中权值最小的边
            System.out.println("("+adjvex[k]+","+k+")");
            //将当前顶点的权值设置为0,表示此顶点已经加入生成树
            lowcost[k] = 0;
            for(j =1; j < graph.getNumOfVertex(); j++) {
                //若下标为k的顶点各边权值小于此前这些顶点未被加入生成树权值
                if (lowcost[j] != 0 && edges[k][j] < lowcost[j]) {
                    //将较小权值存入lowcost
                    lowcost[j] = edges[k][j];
                    //将下标为k的顶点存入adjvex
                    adjvex[j] = k;
                }
            }
        }
    }

测试程序

        int n = 9;
        String[] vertex = {"v0","v1","v2","v3","v4","v5","v6","v7","v8"};
        MyGraph graph = new MyGraph(n);
        for (String string : vertex) {
            graph.insertVertex(string);
        }
        
        
        graph.insertEdge(0, 1, 10);
        graph.insertEdge(0, 5, 11);
        graph.insertEdge(1, 2, 18);     
        graph.insertEdge(1, 6, 16);
        graph.insertEdge(1, 8, 12);
        graph.insertEdge(2, 8, 8);
        graph.insertEdge(2, 3, 22);
        graph.insertEdge(3, 8, 21);
        graph.insertEdge(3, 6, 24);
        graph.insertEdge(3, 4, 20);
        graph.insertEdge(3, 7, 16);
        graph.insertEdge(4, 5, 26);
        graph.insertEdge(4, 7, 7);
        graph.insertEdge(5, 6, 17);
        graph.insertEdge(6, 7, 19);
        
        graph.MiniSpanTree_Prim(graph);

测试结果:


普里姆算法测试结果图.png

克鲁斯卡尔算法(Kruskal)

思想:将图的存储结构使用边集数组的形式表示,并将边集数组按权值从小到大排序,遍历边集数组,每次选取一条边并判断是否构成环路,不会构成环路则将其加入最小生成树,最终只会包含n-1条边(n为无向图的顶点数)。

边集数组的结构如图所示:


边集数组结构图.png

边集数组类

public class Edge implements Comparable {
        int begin;
        int end;
        int weight;
        public int getBegin() {
            return begin;
        }
        public void setBegin(int begin) {
            this.begin = begin;
        }
        public int getEnd() {
            return end;
        }
        public void setEnd(int end) {
            this.end = end;
        }
        public int getWeight() {
            return weight;
        }
        public void setWeight(int weight) {
            this.weight = weight;
        }
        @Override
        public int compareTo(Edge o) {
            // TODO Auto-generated method stub
            //按权值升序排列
            return this.weight - o.weight;
        }
        
}

图类

public class Graph {
    private ArrayList vertexList;       //存放顶点的数组
    Edge[] edges;                       //边集数组
    int[][] arr;                                //邻接矩阵

    public Graph(int v, int e) {
        //初始化结点数组
        vertexList = new ArrayList<>(v);
        //根据边数初始化边集数组
        edges = new Edge[e];
        //向边集数组添加空对象
        for(int i = 0; i < e; i++){
            Edge edge = new Edge();
            edges[i] = edge;
        }
        //初始化邻接矩阵
        arr = new int[v][v];
        for (int i = 0; i < v; i++) {
            for (int j = 0; j < v; j++) {
                if (i == j)
                    arr[i][j] = 0;
                else {
                    arr[j][i] = 65535;
                }
            }
        }
    }
    
    //插入结点
    public void insertVertex(Object vertex) {
        vertexList.add(vertexList.size(),vertex);
    }
    
    //插入无向边以及设置权值
    public void insertEdge(int n1, int n2, int weight) {
        arr[n1][n2] = weight;
        //该图为无向图,所以矩阵关于对角线对称
        arr[n2][n1] = weight;
    }
    
    //获取顶点个数
    public int getNumOfVertex() {
        return vertexList.size();
    }
    
    //获取顶点n的值
    public Object getValueByIndex(int n) {
        return vertexList.get(n);
    }
    
    //邻接矩阵转换为边集数组
    public  void MatrixToEdgesArray() {
        int k = 0;
        //因为该图为无向图,所以该矩阵关于从左上到右下的对角线对称
        //因此此处遍历矩阵只需遍历右上部分即可
        for (int i = 0; i < getNumOfVertex(); i++) {
            for (int j = i ; j < getNumOfVertex(); j++) {
                if (arr[i][j] < 65535 && arr[i][j] != 0) {
                    edges[k].begin = i; // 编号较小的结点为首
                    edges[k].end = j; // 编号较大的结点为尾
                    edges[k].weight = arr[i][j];
                    k++;
                }
            }
        }
        //将edges边集数组按权值从小到大排序
        Arrays.sort(edges);
    }
    
    //最小生成树--克鲁斯卡尔算法
    public void MiniSpanTree_Kruskal(Graph graph) {
        int i, n, m;
        //将邻接矩阵转换为边集数组
        MatrixToEdgesArray();
        int[]  parent = new int[edges.length];
        //初始化parent数组,用于判断是否产生了环路
        for(i = 0; i < edges.length; i++) {
            parent[i] = 0;
        }
        for(i = 0;  i < edges.length; i++) {
            //按权值从小到大拿到每一条边
            Edge edge = edges[i];
            n = Find(parent,edge.begin);
            m = Find(parent,edge.end);
            //n==m时表示构成了环路,不能纳入最小生成树中
            if (n != m) {
                System.out.println("(" + edge.begin + "," + edge.end + ")-->" + edge.weight);
                parent[n] = m;
            }
        }
    }
    
    public int Find(int[] parent, int f) {
        /**当i=7时,parent数组为{1,5,8,7,7,8,0,0,6}
        *               对应的下标为{0,1,2,3,4,5,6,7,8}
        *parent[0] = 1表示顶点0,1已经加入生成树中
        *所以此时顶点0,1,2,5,8,6在一个边集合中;顶点3,4,7在一个边集合中
        **/
        while(parent[f] > 0) {
            f = parent[f];
        }
        return f;
    }
}
 
 

测试程序:

        int n = 9, e = 15;
        String[] vertex = {"v0","v1","v2","v3","v4","v5","v6","v7","v8"};
        Graph graph = new Graph(n,e);
        for (String string : vertex) {
            graph.insertVertex(string);
        }
                
        graph.insertEdge(0, 1, 10);
        graph.insertEdge(0, 5, 11);
        graph.insertEdge(1, 2, 18);     
        graph.insertEdge(1, 6, 16);
        graph.insertEdge(1, 8, 12);
        graph.insertEdge(2, 8, 8);
        graph.insertEdge(2, 3, 22);
        graph.insertEdge(3, 8, 21);
        graph.insertEdge(3, 6, 24);
        graph.insertEdge(3, 4, 20);
        graph.insertEdge(3, 7, 16);
        graph.insertEdge(4, 5, 26);
        graph.insertEdge(4, 7, 7);
        graph.insertEdge(5, 6, 17);
        graph.insertEdge(6, 7, 19);
        
        graph.MiniSpanTree_Kruskal(graph);

测试结果:


克鲁斯卡尔算法测试结果图.png

最小生成树为:


最小生成树.png

总结

普里姆算法针对顶点展开,通过不断寻找与已构建的生成树的最小边来不断构建新的生成树。普里姆算法对于稠密图,也就是边数非常多的情况会更好一些,因为其是通过顶点来展开的。算法时间损耗主要来源于嵌套的for循环,所以时间复杂度为O(n^2)。

克鲁斯卡尔算法针对边展开,通过对边集数组的遍历来构建最小生成树,但是过程中必须避免构成环路。克鲁斯卡尔算法对于稀疏图,也就是边数较少的情况效率会很高。此算法的Find函数由边数e决定,时间复杂度为O(loge),再加上外层for循环的e次,所以时间复杂度为O(eloge)。

你可能感兴趣的:(图的最小生成树算法(Prim和Kruskal))