【数据结构与算法】克鲁斯卡尔算法的介绍和公交站问题程序实现

1. 克鲁斯卡尔算法的介绍

和前面普里姆算法的介绍和修路问题程序实现的修路问题一样。克鲁斯卡尔(Kruskal)算法,也是用来求完全图的最小生成树的算法

基本思想:按照权重值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路

具体做法:先将各条边按权重值从小到大进行排序。从最小权重值的边开始,依次将各个边添加到生成树中。如果一条边添加到树中使树变成回路,则放弃该边的添加

2. 克鲁斯卡尔算法的原理

我们以公交站问题来说明克鲁斯卡尔算法的原理。用数组minTreeEdges保存最小生成树结果

克鲁斯卡尔算法的原理
首先将所有边按权重值从小到大进行排序,然后执行如下步骤:

  1. 第1步:边<E,F>的权重值最小,因此将边<E,F>加入到最小生成树minTreeEdges中
  2. 第2步:上一步操作之后,边<C,D>的权重值最小,因此将边<E,F>加入到最小生成树minTreeEdges中
  3. 第3步:上一步操作之后,边<D,E>的权重值最小,因此将边<D,E>加入到最小生成树minTreeEdges中
  4. 第4步:上一步操作之后,边<C,E>的权重值最小,但<C,E>会和已有的子树构成回路;因此跳过边<C,E>。同理跳过边<C,F>。将边<B,F>加入到最小生成树minTreeEdges中
  5. 第5步:上一步操作之后,边<E,G>的权重值最小,因此将边<E,G>加入到最小生成树minTreeEdges中
  6. 第6步:上一步操作之后,边<F,G>的权重值最小,但<F,G>会和已有的子树构成回路;因此跳过边<F,G>。同理跳过边<B,C>。将边<A,B>加入到最小生成树minTreeEdges中

此时最小生成树构造完成。它包括的边依次是:<E,F>、<C,D>、<D,E>、<B,F>、<E,G>、<A,B>

2.1 将边添加到已有生成树之前,判断是否形成了回路

生成树的终点:就是将所有顶点按照从小到大的顺序排列之后;某个顶点的终点就是该顶点在已有生成树的最大顶点

基本原理:一条边较小的顶点为起点,较大的顶点为终点。得到起点所在已有生成树的最大字符的顶点V1,得到终点所在已有生成树的最大字符的顶点V2,如果V1和V2相等,则表示起点和终点已经在同一颗生成树中,如果将起点和终点连通,则会形成回路。如果V1和V2不相等,则表示起点和终点在不同的生成树中,可以将该边添加到已有生成树中

示例说明
回路判断
如上所示:将<E,F>、<C,D>、<D,E>加入到最小生成树minTreeEdges中之后,这颗生成树的终点都是F

因此,接下来虽然<C,E>是权值最小的边。但是C和E的终点都是F,即它们的终点相同,因此会形成回路

3. 公交站问题的介绍

和前面普里姆算法的介绍和修路问题程序实现的修路问题一样,我们来看一个公交站问题,如下所示:

公交站问题
问题:有7个公交站A、B、C、D、E、F、G,各个公交站之间的距离(权)用边线表示,比如A -> B距离12公里。如何让各个公交站都能连通,并且总的修路里程最短

程序如下:

import java.util.Arrays;

public class KruskalCase {

    // 边的个数
    private int edgeNum;
    // 顶点集合
    private char[] vertexs;
    // N个顶点形成的N * N二维数组,用来保存顶点之间的距离
    private int[][] weights;

    // 用INFINITY表示距离无限大,不能连通
    private static final int INFINITY = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};

        // N个顶点形成的N * N二维数组,用来保存顶点之间的距离
        // 用INFINITY表示距离无限大,不能连通
        int[][] weights = {
                {0, 12, INFINITY, INFINITY, INFINITY, 16, 14},
                {12, 0, 10, INFINITY, INFINITY, 7, INFINITY},
                {INFINITY, 10, 0, 3, 5, 6, INFINITY},
                {INFINITY, INFINITY, 3, 0, 4, INFINITY, INFINITY},
                {INFINITY, INFINITY, 5, 4, 0, 2, 8},
                {16, 7, 6, INFINITY, 2, 0, 9},
                {14, INFINITY, INFINITY, INFINITY, 8, 9, 0}
        };

        // 创建KruskalCase对象
        KruskalCase kruskalCase = new KruskalCase(vertexs, weights);

        // 输出weights
        kruskalCase.show();

        // 测试克鲁斯卡尔算法
        kruskalCase.kruskal();
    }

    // 构造器
    public KruskalCase(char[] vertexs, int[][] weights) {
        // 顶点数
        int vertexNum = vertexs.length;

        // 初始化顶点, 并进行赋值
        this.vertexs = new char[vertexNum];
        for (int i = 0; i < vertexs.length; i++) {
            this.vertexs[i] = vertexs[i];
        }

        // 初始化二维数组,并进行赋值
        this.weights = new int[vertexNum][vertexNum];
        for (int i = 0; i < vertexNum; i++) {
            for (int j = 0; j < vertexNum; j++) {
                this.weights[i][j] = weights[i][j];
            }
        }

        // 统计边的条数
        for (int i = 0; i < vertexNum; i++) {
            // 只统计右上三角的
            for (int j = i + 1; j < vertexNum; j++) {
                if (this.weights[i][j] != INFINITY) {
                    edgeNum++;
                }
            }
        }
    }

    // 显示二维数组,即显示顶点之间的距离
    public void show() {
        System.out.println("二维数组为: ");
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = 0; j < vertexs.length; j++) {
                System.out.printf("%12d", weights[i][j]);
            }
            System.out.println();
        }
    }

    // 从weights二维数组中,形成Edge数组
    private Edge[] getEdges() {
        Edge[] edges = new Edge[edgeNum];
        // edges的index从0开始
        int edgeIndex = 0;

        for (int i = 0; i < vertexs.length; i++) {
            // 只处理右上三角的
            for (int j = i + 1; j < vertexs.length; j++) {
                if (weights[i][j] != INFINITY) {
                    // 因为处理的是右上三角的,所以i比j小,正好对应起点和终点
                    edges[edgeIndex++] = new Edge(vertexs[i], vertexs[j], weights[i][j]);
                }
            }
        }
        return edges;
    }

    // 使用冒泡排序算法,对edges中的边,按权重值由小到大排列
    private void sortEdges(Edge[] edges) {
        Edge tmpEdge = null;

        // 用于控制遍历多少轮
        for (int i = 0; i < edges.length - 1; i++) {
            // 用于控制在一轮中,遍历多少次
            for (int j = 0; j < edges.length - 1 - i; j++) {
                if (edges[j].weight > edges[j + 1].weight) {//交换
                    tmpEdge = edges[j];
                    edges[j] = edges[j + 1];
                    edges[j + 1] = tmpEdge;
                }
            }
        }
    }

    // 根据顶点的值,返回顶点的index。查找不到则返回-1
    private int getVertexIndex(char vertex) {
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == vertex) {
                return i;
            }
        }

        // 找不到, 则返回-1
        return -1;
    }

    // 从vertexEndIndexs找到index为vertexIndex的顶点的终点的index
    private int getVertexEndIndex(int[] vertexEndIndexs, int vertexIndex) {
        // 如果找不到终点,则直接返回该顶点的index
        // 如果找到了终点。可能该终点还有终点,迭代处理。返回最终的终点的index
        while (vertexEndIndexs[vertexIndex] != 0) {
            vertexIndex = vertexEndIndexs[vertexIndex];
        }
        return vertexIndex;
    }

    // 克鲁斯卡尔算法实现
    public void kruskal() {
        // 保存每个顶点在最小生成树的终点的index,这是一个动态的过程
        int[] vertexEndIndexs = new int[vertexs.length];
        // 保存最小生成树的所有边
        Edge[] minTreeEdges = new Edge[vertexs.length - 1];
        int minTreeEdgeIndex = 0;

        // 获取图中所有的边的集合
        Edge[] edges = getEdges();
        System.out.println("图共" + edges.length + "条边,图的边的集合 = " + Arrays.toString(edges));

        // 对edges中的边,按权重值由小到大排列
        sortEdges(edges);

        // 遍历edges数组,将边添加到最小生成树中时。如果没有形成回路,就添加到minTreeEdges, 否则不添加
        for (int i = 0; i < edgeNum; i++) {
            // 获取第i条边的顶点(起点)的index
            int startVertexIndex = getVertexIndex(edges[i].startVertex);
            // 获取第i条边的顶点(终点)的index
            int endVertexIndex = getVertexIndex(edges[i].endVertex);

            // 获取边的顶点(起点)在当前最小生成树的终点index
            int startVertexEndIndex = getVertexEndIndex(vertexEndIndexs, startVertexIndex);
            // 获取边的顶点(终点)在当前最小生成树的终点index
            int endVertexEndIndex = getVertexEndIndex(vertexEndIndexs, endVertexIndex);

            // 如果不构成回路
            if (startVertexEndIndex != endVertexEndIndex) {
                // startVertexEndIndex是一条边的起点所在已有子树A的终点index
                // endVertexEndIndex是该边的终点所在已有子树B的终点index
                // 赋值之前vertexEndIndexs[startVertexEndIndex]的值肯定是0,且startVertexEndIndex小于endVertexEndIndex
                // vertexEndIndexs[startVertexEndIndex] = endVertexEndIndex相当于给子树A设置新的终点index
                vertexEndIndexs[startVertexEndIndex] = endVertexEndIndex;

                // 将符合条件的边,添加到minTreeEdges
                minTreeEdges[minTreeEdgeIndex++] = edges[i];
            }
        }

        // 输出各个顶点的终点index数组
        System.out.println("各个顶点的终点index:" + Arrays.toString(vertexEndIndexs));

        // 输出最小生成树
        System.out.println("最小生成树为:");
        for (int i = 0; i < minTreeEdgeIndex; i++) {
            System.out.println(minTreeEdges[i]);
        }

    }

}

// 用来表示一条边
class Edge {
    // 边的一个顶点(起点)
    char startVertex;
    // 边的一个顶点(终点)
    char endVertex;
    // 边的权重值
    int weight;

    public Edge(char startVertex, char endVertex, int weight) {
        this.startVertex = startVertex;
        this.endVertex = endVertex;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Edge [<" + startVertex + ", " + endVertex + "> = " + weight + "]";
    }

}

运行程序,结果如下:

二维数组为: 
           0          12  2147483647  2147483647  2147483647          16          14
          12           0          10  2147483647  2147483647           7  2147483647
  2147483647          10           0           3           5           6  2147483647
  2147483647  2147483647           3           0           4  2147483647  2147483647
  2147483647  2147483647           5           4           0           2           8
          16           7           6  2147483647           2           0           9
          14  2147483647  2147483647  2147483647           8           9           0
图共12条边,图的边的集合 = [Edge [<A, B> = 12], Edge [<A, F> = 16], Edge [<A, G> = 14], Edge [<B, C> = 10], Edge [<B, F> = 7], Edge [<C, D> = 3], Edge [<C, E> = 5], Edge [<C, F> = 6], Edge [<D, E> = 4], Edge [<E, F> = 2], Edge [<E, G> = 8], Edge [<F, G> = 9]]
各个顶点的终点index:[6, 5, 3, 5, 5, 6, 0]
最小生成树为:
Edge [<E, F> = 2]
Edge [<C, D> = 3]
Edge [<D, E> = 4]
Edge [<B, F> = 7]
Edge [<E, G> = 8]
Edge [<A, B> = 12]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值