当前位置: 首页>后端>正文

14_业界经典算法(Java、Kotlin描述)

????本篇文章介绍业界的一些经典算法。如果算法较长,会省略掉Kotlin版本。

(1)最大子序列和算法

????这里给出四种最大子序列和算法。它们的时间复杂度依次降低。
????第一种,Java版本:

/**
     * O(N的三次方)
     */
    public static int maxSubSum1(int[] a) {
        int maxSum = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = i; j < a.length; j++) {
                int thisSum = 0;
                for (int k = i; k <= j; k++) {
                    thisSum += a[k];
                }
                if (maxSum < thisSum) {
                    maxSum = thisSum;
                }
            }
        }
        return maxSum;
    }

????Kotlin版本:

    fun maxSubSum1(a: IntArray): Int {
        var maxSum = 0
        for (i in a.indices) {
            for (j in i until a.size) {
                var thisSum = 0
                for (k in i..j) {
                    thisSum += a[k]
                }
                if (maxSum < thisSum) {
                    maxSum = thisSum
                }
            }
        }
        return maxSum
    }

????第二种,Java版本

    /**
     * O(N的二次方)
     */
    public static int maxSubSum2(int[] a) {
        int maxSum = 0;
        for (int i = 0; i < a.length; i++) {
            int thisSum = 0;
            for (int j = i; j < a.length; j++) {
                thisSum += a[j];
                if (maxSum < thisSum) {
                    maxSum = thisSum;
                }
            }
        }
        return maxSum;
    }

????Kotlin版本:

    /**
     * O(N的二次方)
     */
    fun maxSubSum2(a: IntArray): Int {
        var maxSum = 0
        for (i in a.indices) {
            var thisSum = 0
            for (j in i until a.size) {
                thisSum += a[j]
                if (maxSum < thisSum) {
                    maxSum = thisSum
                }
            }
        }
        return maxSum
    }

????第三种,采用分治思想实现。Java版本:

    public static int maxSubSum3(int[] a) {
        return maxSumRec(a, 0, a.length - 1);
    }

    /**
     * O(N logN)
     * 分治思想:最大子序列和位于:(1)输入数据的左半部分;(2)右半部分;(3)跨越中部位于左右两半部分之间。
     * 左右部分可以递归求解
     */
    public static int maxSumRec(int[] a, int left, int right) {
        int maxSum = 0;
        if (left == right) {
            if (a[left] > maxSum) {
                maxSum = a[left];
            }
            return maxSum;
        }

        int middle = (left + right) / 2;
        int maxLeftSum = maxSumRec(a, left, middle);
        int maxRightSum = maxSumRec(a, middle + 1, right);

        int leftSum = 0, tmpLeftSum = 0;
        for (int i = middle; i >= 0; i--) {
            tmpLeftSum += a[i];
            if (tmpLeftSum > leftSum) {
                leftSum = tmpLeftSum;
            }
        }

        int rightSum = 0, tmpRightSum = 0;
        for (int i = middle + 1; i <= right; i++) {
            tmpRightSum += a[i];
            if (rightSum < tmpRightSum) {
                rightSum = tmpRightSum;
            }
        }

        if (maxLeftSum > maxRightSum) {
            maxSum = maxLeftSum;
        } else {
            maxSum = maxRightSum;
        }

        if (maxSum < (leftSum + rightSum)) {
            maxSum = leftSum + rightSum;
        }

        return maxSum;
    }

????Kotlin版本:

    fun maxSubSum3(a: IntArray): Int {
        return maxSumRec(a, 0, a.size - 1)
    }

    /**
     * O(N logN)
     * 分治思想:最大子序列和位于:(1)输入数据的左半部分;(2)右半部分;(3)跨越中部位于左右两半部分之间。
     * 左右部分可以递归求解
     */
    fun maxSumRec(a: IntArray, left: Int, right: Int): Int {
        var maxSum = 0
        if (left == right) {
            if (a[left] > maxSum) {
                maxSum = a[left]
            }
            return maxSum
        }
        val middle = (left + right) / 2
        val maxLeftSum = maxSumRec(a, left, middle)
        val maxRightSum = maxSumRec(a, middle + 1, right)
        var leftSum = 0
        var tmpLeftSum = 0
        for (i in middle downTo 0) {
            tmpLeftSum += a[i]
            if (tmpLeftSum > leftSum) {
                leftSum = tmpLeftSum
            }
        }
        var rightSum = 0
        var tmpRightSum = 0
        for (i in middle + 1..right) {
            tmpRightSum += a[i]
            if (rightSum < tmpRightSum) {
                rightSum = tmpRightSum
            }
        }
        maxSum = if (maxLeftSum > maxRightSum) {
            maxLeftSum
        } else {
            maxRightSum
        }
        if (maxSum < leftSum + rightSum) {
            maxSum = leftSum + rightSum
        }
        return maxSum
    }

????第四种,主要思想:不需要知道具体的最佳子序列在哪里;如果a[i]是负值,那么它不可能代表最优序列的起点,因为任何包含a[i]作为起点的子序列都可以通过用a[i+1]来得到改进;类似的,任何负的子序列不可能是最优子序列的前缀,原理相同.
????Java版本:

    /**
     * O(N)
     */
    public static int maxSubSum4(int[] a) {
        int maxSum = 0;
        int thisSum = 0;
        for (int i = 0; i < a.length; i++) {
            thisSum += a[i];
            if (thisSum > maxSum) {
                maxSum = thisSum;
            } else if (thisSum < 0) {
                thisSum = 0;
            }
        }
        return maxSum;
    }

????Kotlin版本:

    fun maxSubSum4(a: IntArray): Int {
        var maxSum = 0
        var thisSum = 0
        for (i in a.indices) {
            thisSum += a[i]
            if (thisSum > maxSum) {
                maxSum = thisSum
            } else if (thisSum < 0) {
                thisSum = 0
            }
        }
        return maxSum
    }

(2) 霍夫曼编码

????霍夫曼编码(Huffman)编码是为了解决如何构造一种特殊的编码树而设计的编码方案。在介绍它之前,先来了解一下文件压缩。
????这里说的文件压缩,是指无损压缩,可以解压还原。对于一些比较大的文件,如果可以无损压缩它,不仅能节省存储空间,进行网络传输时还能更快。一种想法是采用变长编码,出现频繁的字符采用较短的编码,从而达到压缩的目的。实现这种想法所采用的数据结构是满二叉树(若不是,通过增加或删除非叶节点来满足),树中只有叶子节点有数据(字符)。从根节点开始,通往每个叶子节点时,用0表示左分支,1表示右分支,这样就可以得到相应的路径,这个路径就是该字符的编码。这里有一个关键问题是:任何字符的编码,不能是其他字符编码的前缀,否则就分不清了。哈夫曼编码就是满足所有条件的一种解决方案。
????主要思路:假设字符个数为C,开始时,每个字符都看作是包含一个元素的树,所有的字符就组成了一个森林;一颗树的权等于它所有的树叶频率之和;任意选取最小权的树T1、T2,将它们合并为一个新树,T1、T2作为新树的叶子节点;将这样的过程进行(C-1)次,就得到了Huffman编码树。
????举例说明,假设一个文件由“天”、“地”、“人”三字组成(乱序),出现次数分别为3w、2w、1w,三字对应的Huffman编码依次是“1”、"01"、“00”(比特,位)。如果以一个汉字占2字节来计算,那么压缩前需要的空间:12w字节= 117 KB。压缩后的空间:10w 比特 = 12 KB。可以看到,效果明显。只要解压缩一方知道编码方案,就可以解压还原了。
????Java实现:

/**
 * 霍夫曼算法
 */
public class Hufman {

    //huffman编码方案
    static Map<Character, String> huffmanCodes = new HashMap<>();

    class Node implements Comparable<Node> {
        Character data;//存放字符
        int weight; //权值
        Node leftChild; //左孩子
        Node rightChild; //右孩子

        public Node(Character data, int weight) {
            this.data = data;
            this.weight = weight;
        }

        @Override
        public int compareTo(Node o) {
            return this.weight - o.weight;
        }
    }

    /**
     * 从输入中,构造所有的节点
     *
     * @param content
     * @return
     */
    List<Node> getNodes(String content) {
        List<Node> nodeList = new ArrayList<>();
        if (content != null && content.length() > 0) {
            char[] chars = content.toCharArray();
            Map<Character, Integer> nodeWeightMap = new HashMap<>();
            for (char charTmp : chars) {
                Integer count = nodeWeightMap.get(charTmp);
                if (count == null) {
                    nodeWeightMap.put(charTmp, 1);
                } else {
                    nodeWeightMap.put(charTmp, ++count);
                }
            }
            Set<Character> keySet = nodeWeightMap.keySet();
            for (Character key : keySet) {
                Node node = new Node(key, nodeWeightMap.get(key));
                nodeList.add(node);
            }
        }
        return nodeList;
    }

    /**
     * 创建Huffman树
     *
     * @param nodeList
     * @return
     */
    Node createHuffmanTree(List<Node> nodeList) {
        while (nodeList.size() > 1) {
            Collections.sort(nodeList);
            Node left = nodeList.remove(0);
            Node right = nodeList.remove(0);
            Node parent = new Node(null, left.weight + right.weight);
            parent.leftChild = left;
            parent.rightChild = right;
            nodeList.add(parent);
        }
        return nodeList.get(0);
    }

    /**
     * 为每个叶子节点,创建huffman编码
     *
     * @param node    节点
     * @param doneCode 已完成的huffman编码,用于拼接
     */
    void createHuffmanCode(Node node, String doneCode) {
        if (node != null) {
            if (node.data == null) { //非叶节点
                createHuffmanCode(node.leftChild, doneCode +"0");
                createHuffmanCode(node.rightChild, doneCode +"1");
            } else { //叶子节点
                huffmanCodes.put(node.data, doneCode);
            }
        }
    }

    public static void main(String[] args) {
        Hufman hufman = new Hufman();
        String content = "aabbbbbbccc中中中中???????";
        List<Node> nodeList = hufman.getNodes(content);
        Node rootNode = hufman.createHuffmanTree(nodeList);
        hufman.createHuffmanCode(rootNode, "");
        System.out.println(huffmanCodes);
    }

}

????输出编码方案:

{?=11, a=010, b=10, c=011, 中=00}

????本示例中,是可以处理常用中文字符的。或者说,可以处理位于BMP(基本多文种平面)中的中文汉字,数量大概有2W+,是一些常用的汉字。对于其他平面的汉字,是不支持的。emoji等因为不在BMP中,所以也不支持。
????汉字的数量远超英文字符,有7W+,常用的有2W+。所以,对于包含很多汉字的大文件来说,霍夫曼树是非常庞大的。算法的耗时估计不容乐观。但考虑到这个算法是1955年提出的,只考虑英文字母的情况下,最多26+26=52字符加上标点符号,还是非常合适的。
????Huffman编码到现在还没有过时,gzip压缩算法就是将它和另外一个压缩算法LZ77组合起来的。LZ77算法是1977年提出的,L、Z分别代表2个作者,这里不做扩充了。
????上面说到,本示例中可以处理中文。网上、书上不少版本,是不支持处理中文的。这里做一些说明。请看构造节点时getNodes()方法里的这一句:

char[] chars = content.toCharArray();

???? 后面的处理都是基于char类型的,Java中的char类型,占2个字节,包含了BMP中所有的字符。所以,它能处理常见的中文字符,但不能处理非BMP字符。
???? 如果将上面这一句,改为byte[]的方式,后续的也做相应修改,那么就不能处理任何中文字符了,如下:

byte[] bytes = content.getBytes();

???? 在网络传输的场景中,一般都处理的字节byte,此时,如果想处理常见中文字符,那么可以将InputStream转换为InputStreamReader,然后以char的方式处理。
???? 让我们再进一步,如果确实要处理非BMP字符,该怎么修改?在一些emoji表情的场景和非常见汉字,还是可能遇到的。这里简单说一下思路。

???? 在基于char的处理上,对获取的每个char,做一下判断。Character中有个方法isHighSurrogate(),可以判断当前char是否是高位代理,如果是,表明这个char字符是非BMP平面某个字符的一半,接着可以获取下个字符,然后将两者组合成一个新字符。当然,此时不能再用char来表示,因为超出了char的表示范围,而必须用字符串String。所以相应的数据类型也要改变。组成新的字符串方式如下:
???? char highSur = ...;
???? char lowSur = ...;
???? char[] chars = {highSur,lowSur};
???? String highLowStr = new String(chars);

????注意,高位在前,低位在后。
????从上面的过程可以知道,每个压缩文件对应的霍夫曼编码都不一样。类似这种特点的编码称为动态编码,有动即有静,静态编码是指保持不变的编码。静态编码是更常见的,如UTF-8、UTF-16、GBK等。霍夫曼编码也有静态版的,但适用性就没那么强了。
????霍夫曼编码属于“贪婪算法”这一类型,它的特点是分阶段地工作,“眼下能拿到的就拿”,形成局部最优。在构造霍夫曼树时,每次都选择权值最小的2个树进行合并,获得局部最优解,直至构造完成。

(3)Dijkstra算法

????Dijkstra(迪克斯特拉)算法是图论中一种解决单源最短路径问题的算法,是贪婪算法中的一种。
????主要思想:从源点S出发,按阶段进行,每个阶段,选择一个邻接新顶点(未被访问过),使得S到它的距离最短。
????本小节将介绍两种实现方式,一种是使用邻接矩阵,一种是使用优先队列。
????先来看看邻接矩阵的方式,使用了《09_算法基本概念》中的图结构。Java版本如下:

/**
 * Dijkstra算法
 */
public class Dijkstra {
    //一般的图结构
    class Graph {
        static final int NUM = 10; //图的最大顶点数
        char[] vertex = new char[NUM]; //顶点集合
        int vertexNum;//实际顶点数量
        int[][] edgeWeight = new int[NUM][NUM]; //邻接矩阵,含权
        boolean[] isTrav = new boolean[NUM]; //是否访问的标志
    }

    /***
     * 在未访问的连通顶点中,找到一个距离最小的,返回下标。
     *
     * 只看这个方法的话,是不能确定找到的这个顶点与已选择的顶点是连通的。
     * 但结合上文来看,初始化时,已设置了源点到自身的距离是0,也即distArray数组中有一项是0。
     * 而这个原点并未设置成已访问的,所以这个方法在第一次调用时返回的下标就是源点下标。
     * 从下文来看,随后会更新与该顶点有连通的顶点距离放到distArray数组中,
     * 后续对该方法的调用,其实就是在连通顶点中选择最小值。
     * 
     * @param graph 图
     * @param distArray 已选择的顶点距离数组
     * @return
     */
    int minDist(Graph graph, int[] distArray) {
        int min = Integer.MAX_VALUE;
        int minIndex = -1;
        for (int j = 0; j < graph.vertexNum; j++) {
            if (!graph.isTrav[j] && distArray[j] <= min ) {
                min = distArray[j];
                minIndex = j;
            }
        }
        return minIndex;
    }

    /**
     * Dijkstra算法实现
     *
     * @param srcVertex 源顶点
     * @param graph     图结构
     */
    void dijkstra(char srcVertex, Graph graph) {
        //各个顶点到源顶点的最小距离
        int[] distArray = new int[graph.vertexNum];

        //初始化
        for (int i = 0; i < graph.vertexNum; i++) {
            distArray[i] = Integer.MAX_VALUE;
        }

        //获取源顶点的下标
        int srcIndex = getSrcIndex(srcVertex, graph);
        distArray[srcIndex] = 0;//到自己的距离永远为0

        for (int i = 0; i < graph.vertexNum - 1; i++) {
            int uIndex = minDist(graph, distArray);
            graph.isTrav[uIndex] = true;

            //处理当前顶点的邻接顶点,更新未选中顶点的距离
            for (int j = 0; j < graph.vertexNum; j++) {
                if (!graph.isTrav[j] && graph.edgeWeight[i][j] != 0 && distArray[uIndex] != Integer.MAX_VALUE
                        && distArray[uIndex] + graph.edgeWeight[uIndex][j] < distArray[j] ) {
                    distArray[j] = distArray[uIndex] + graph.edgeWeight[uIndex][j];
                }
            }
        }

        //打印最小距离
        printMinDist(distArray, srcVertex ,graph);
    }

    void printMinDist(int[] distArray, char srcVertex,Graph graph) {
        System.out.println("源点是:"+srcVertex);
        System.out.println("顶点 \t到源点的最短距离");
        for (int i = 0; i < distArray.length; i++)
            System.out.println(graph.vertex[i] + " \t\t" + distArray[i]);
    }

    /**
     * 找到指定顶点在图中的下标index
     *
     * @param srcVertex
     * @param graph
     * @return
     */
    int getSrcIndex(char srcVertex, Graph graph) {
        int index = -1;
        for (int i = 0; i < graph.vertexNum; i++) {
            if (graph.vertex[i] == srcVertex) {
                index = i;
                break;
            }
        }
        return index;
    }

    public static void main(String[] args) {
        Dijkstra dijkstra = new Dijkstra();

        char src = 'A';
        Graph graph = dijkstra.createGraph(src);
        dijkstra.dijkstra(src,graph);
    }

    /**
     * 构造一个图
     * @param src 顶点
     * @return
     */
    private Graph createGraph(char src) {
        Graph graph = new Graph();
        graph.vertexNum = 6;
        graph.edgeWeight= new int[][]{
                {0, 1, 3, 0, 0, 0},
                {1, 0, 0, 4, 0, 0},
                {3, 0, 0, 0, 2, 0},
                {0, 4, 0, 0, 0, 10},
                {0, 0, 2, 0, 0, 9},
                {0, 0, 0, 10, 9, 0}};
        graph.vertex = new char[]{'A', 'B', 'C', 'D', 'E', 'F'};
        return graph;
    }

}

????上面算法构造的图如下:


14_业界经典算法(Java、Kotlin描述),第1张
构造图

????输出结果:

源点是:A
顶点????到源点的最短距离
A???????0
B???????1
C???????3
D???????5
E???????5
F???????14

????再来介绍优先队列的方式。在上面的代码中,找到一个与已访问顶点的最小连通顶点,是通过方法minDist()来实现的。而获取最小者,这非常符合优先队列的特点。优先队列是删除最小者,但删除的同时,也获得了这个最小者。因此,可以通过优先队列来处理。Java版本:

/**
 * PriorityQueue实现Dijkstra算法
 */
public class DijkstraPQ {

    /**
     * 节点Node,如A、B、C等
     */
    static class Node implements Comparator<Node> {
        char aChar; //顶点名称
        int weight; //权值

        public Node() {
        }

        public Node(char aChar, int weight) {
            this.aChar = aChar;
            this.weight = weight;
        }

        @Override
        public int compare(Node node1, Node node2) {

            return node1.weight - node2.weight;
        }
    }

    int distArray[];
    PriorityQueue<Node> priorityQueue;
    Set<Character> visited;
    int vertexNum;//顶点总数
    List<List<Node>> adjList;//邻接矩阵,改用List来表示,上面是用二维数组
    List<Character> charList;//所有的节点list

    public DijkstraPQ(int vertexNum) {
        this.vertexNum = vertexNum;
        distArray = new int[vertexNum];
        visited = new HashSet<>();
        priorityQueue = new PriorityQueue<Node>(vertexNum, new Node());
        charList = new ArrayList<>();
    }

    void dijkstra(List<List<Node>> adjList, char srcVertex) {
        this.adjList = adjList;
        for (int i = 0; i < vertexNum; i++) {
            distArray[i] = Integer.MAX_VALUE;
        }
        priorityQueue.add(new Node(srcVertex, 0)); //将源点加入优先队列
        int srcIndex = getNodeIndex(srcVertex);
        distArray[srcIndex] = 0; //源点到自身的权值为0

        while (visited.size() != vertexNum) {
            Node node = priorityQueue.remove(); //距离最小的Node
            visited.add(node.aChar);

            int index = getNodeIndex(node.aChar);
            for (int i = 0; i < adjList.get(index).size(); i++) {
                Node tmpNode = adjList.get(index).get(i);
                int tmpIndex = getNodeIndex(tmpNode.aChar);
                if (!visited.contains(tmpNode.aChar)) {
                    int newDist = distArray[index] + tmpNode.weight;
                    if (newDist < distArray[tmpIndex]) {
                        distArray[tmpIndex] = newDist;
                    }
                    priorityQueue.add(new Node(tmpNode.aChar, distArray[tmpIndex]));
                }
            }
        }
        printResults(srcVertex);
    }

    void printResults(char srcChar) {
        System.out.println("源点是:" + srcChar);
        System.out.println("顶点 \t到源点的最短距离");
        for (int i = 0; i < distArray.length; i++)
            System.out.println(charList.get(i) + " \t\t" + distArray[i]);
    }

    /**
     * 获取下标
     *
     * @return
     */
    int getNodeIndex(char aChar) {
        int index = -1;
        for (int i = 0; i < charList.size(); i++) {
            if (aChar == charList.get(i)) {
                index = i;
                break;
            }
        }
        return index;
    }

    public static void main(String[] args) {
        int num = 6;
        char srcChar = 'A';
        DijkstraPQ dijkstraTwo = new DijkstraPQ(6);
        dijkstraTwo.charList.add('A');
        dijkstraTwo.charList.add('B');
        dijkstraTwo.charList.add('C');
        dijkstraTwo.charList.add('D');
        dijkstraTwo.charList.add('E');
        dijkstraTwo.charList.add('F');

        List<List<Node>> adjList = new ArrayList<List<Node>>();
        for (int i = 0; i < num; i++) {
            List<Node> item = new ArrayList<>();
            adjList.add(item);
        }

        //邻接矩阵
        adjList.get(0).add(new Node('B',1));
        adjList.get(0).add(new Node('C',3));
        adjList.get(1).add(new Node('A',1));
        adjList.get(1).add(new Node('D',4));
        adjList.get(2).add(new Node('A',3));
        adjList.get(2).add(new Node('E',2));
        adjList.get(3).add(new Node('B',4));
        adjList.get(3).add(new Node('F',10));
        adjList.get(4).add(new Node('C',2));
        adjList.get(4).add(new Node('F',9));
        adjList.get(5).add(new Node('D',10));
        adjList.get(5).add(new Node('E',9));

        dijkstraTwo.dijkstra(adjList,srcChar);
    }
}

????邻接矩阵的数据,仍然是基于上面的构造图。输出结果也和上面是一致的。
????List<List<>>表示的邻接矩阵,开始不那么容易理解。以这条语句为例来进行说明:

adjList.get(0).add(new Node('B',1));
它表示顶点'A'到顶点'B'的连接(有方向),权值是1;等价的二维数组表示法:edgeWeight[0][1] = 1;
顶点是按照一定的顺序确定好的。0对应着'A','B'对应着1。
adjList.get(1).add(new Node('A',1));
这行代码表示'B'到'A'有一条连接,权值也是1。等价的二维数组表示法:edgeWeight[1][0] = 1;
这是因为使用的构造图是无方向的,正反都得来一次。

????List<List<>>形式表示的邻接矩阵,在表示有向图时非常方便。一个Node代表着一条带箭头的边。而表示无向图时,显然二维数组要更方便,沿着对角线(左上到右下)是完全对称的。这两种表示方式可以相互替换,稍加改动即可。
????此外,基于优先队列的这种特点,前面一小节(2)霍夫曼编码在选取两个权值最小的树时,也可以考虑使用优先队列来实现,不过在此就不展开了。

(4)Prim算法

????Prim算法是图论中找出最小生成树的一种算法。最小生成树的概念可以去《09_算法基本概念》复习一遍。
????基本思想:它和Dijstra算法有很大的相似性;初始时,随机选择一个顶点作为最小生成树的根;接着选择到生成树权值最小的连通顶点,这一过程和Dijstra算法一样;直到所有的顶点都处理完为止。
????Java实现:

/**
 * Prim算法:最小生成树算法
 */
public class Prim {
    //一般的图结构
    class Graph {
        static final int NUM = 10; //图的最大顶点数
        char[] vertex = new char[NUM]; //顶点集合
        int vertexNum;//实际顶点数量
        int gType = 0; //无向图
        int[][] edgeWeight = new int[NUM][NUM]; //邻接矩阵,含权
        boolean[] isTrav = new boolean[NUM]; //是否访问的标志
    }

    /**
     * prim算法
     *
     * @param graph 原始图
     * @return 最小生成树组成的图
     */
    Graph prim(Graph graph) {
        Graph resultG = new Graph(); //目标最小生成树,用图结构表示
        resultG.vertex = graph.vertex; //最终顶点是一样的
        resultG.vertexNum = graph.vertexNum; //最终顶点数量也是一样的

        int[] distArray = new int[graph.vertexNum]; //各未选顶点到最小生成树的最小距离
        int[] mstIndexArray = new int[graph.vertexNum];//各未选顶点连接到最小生成树顶点的下标
        for (int i = 0; i < distArray.length; i++) { //初始值设为最大
            distArray[i] = Integer.MAX_VALUE;
        }

        //任选一点作为起始顶点
        distArray[0] = 0;//到自己的距离为0

        for (int i = 0; i < graph.vertexNum; i++) {
            //找到一个权值最小的顶点
            int minIndex = minDist(graph, distArray);
            graph.isTrav[minIndex] = true;
            for (int j = 0; j < graph.vertexNum; j++) {
                if (!graph.isTrav[j] && graph.edgeWeight[minIndex][j] != 0 && graph.edgeWeight[minIndex][j] < distArray[j]) {
                    distArray[j] = graph.edgeWeight[minIndex][j];
                    mstIndexArray[j] = minIndex;
                    resultG.edgeWeight[minIndex][j] = graph.edgeWeight[minIndex][j];
                }
            }
        }
        printResult(graph, mstIndexArray);
        return resultG;
    }

    private void printResult(Graph graph, int[] mstIndexArray) {
        System.out.println("顶点对\t\t\t\t权值");
        for (int i = 0; i < graph.vertexNum; i++) {
            int mstIndex = mstIndexArray[i];
            if (i != mstIndex && graph.edgeWeight[i][mstIndex] != 0) {
                System.out.println(graph.vertex[mstIndex] + " <------> " + graph.vertex[i] + "\t\t" + graph.edgeWeight[i][mstIndex]);
            }
        }
    }

    /**
     * 在未访问的连通顶点中,找到一个距离最小的,返回下标。
     *
     * @param graph 源图
     * @return
     */
    private int minDist(Graph graph, int[] distArray) {
        int min = Integer.MAX_VALUE;
        int minIndex = -1;
        for (int j = 0; j < graph.vertexNum; j++) {
            if (!graph.isTrav[j] && distArray[j] <= min) {
                min = distArray[j];
                minIndex = j;
            }
        }
        return minIndex;
    }

    public static void main(String[] args) {
        Prim prim = new Prim();
        Graph graph = prim.createGraph();
        Graph resultG = prim.prim(graph);

        System.out.println("顶点对\t\t\t\t权值");
        for (int i = 0; i < resultG.vertexNum; i++) {
            for (int j = i + 1; j < resultG.vertexNum; j++) {
                if (resultG.edgeWeight[i][j] != 0) {
                    System.out.println(resultG.vertex[i] + " <------> " + resultG.vertex[j] + "\t\t" + resultG.edgeWeight[i][j]);
                }
            }
        }
    }

    /**
     * 构造一个图
     *
     * @return
     */
    Graph createGraph() {
        Graph graph = new Graph();
        graph.vertexNum = 6;
        graph.edgeWeight = new int[][]{
                {0, 1, 3, 0, 0, 0},
                {1, 0, 0, 4, 0, 0},
                {3, 0, 0, 0, 2, 0},
                {0, 4, 0, 0, 0, 10},
                {0, 0, 2, 0, 0, 9},
                {0, 0, 0, 10, 9, 0}};
        graph.vertex = new char[]{'A', 'B', 'C', 'D', 'E', 'F'};
        return graph;
    }
}

????输出结果:

顶点对??????????权值
A <------> B????1
A <------> C????3
B <------> D????4
C <------> E????2
E <------> F????9
顶点对??????????权值
A <------> B????1
A <------> C????3
B <------> D????4
C <------> E????2
E <------> F????9

????这里输出了二次,结果都一样。主要是为了对比,前一次是使用各顶点连接到最小生成树的顶点下标数组来输出的;后一次是使用prim()方法的返回值,即用图结构表示的最小生成树,然后在此基础上输出的,这个返回值也可以不需要,只是为了获得一颗最小生成树而已。
????它和Dijkstra算法的主要区别一是需要多记录各顶点到生成树的顶点下标,其次是更新未选顶点的距离时,只需权值即可,不再加上已选顶点的距离。对比如下:

dijkstra算法,需要加上已选顶点距离:
distArray[j] = distArray[minIndex] + graph.edgeWeight[minIndex][j];
而prim算法,只需要计算权值:
distArray[j] = graph.edgeWeight[minIndex][j];

????此外,既然是树,那么如果想用树结构来表示呢?也是可以的。转换如下:

     /**
     * 将图结构表示的最小生成树,转为树结构表示的
     *
     * @param graph 图
     * @return 树
     */
    Node convert(Graph graph) {
        Node[] allNodes = new Node[graph.vertexNum];
        for (int i = 0; i < graph.vertexNum; i++) {
            Node tmpNode = new Node(graph.vertex[i]);
            if (i==0){
                tmpNode.weight = 0;//根节点没有父节点,权值置0
            }
            allNodes[i] = tmpNode;
        }
        for (int i = 0; i < graph.vertexNum - 1; i++) {
            for (int j = i + 1; j < graph.vertexNum; j++) {
                if (graph.edgeWeight[i][j] != 0) {//i到j有连接
                    Node iNode = allNodes[i];
                    Node jNode = allNodes[j];
                    jNode.weight = graph.edgeWeight[i][j];
                    iNode.addChildNode(jNode);//添加为子节点
                }
            }
        }
        return allNodes[0];
    }

    class Node {
        char aChar; //节点名
        List<Node> childNodes; //所有的孩子节点
        int weight;//到父节点的权值
        
        public Node(char aChar) {
            childNodes = new ArrayList<Node>();
            this.aChar = aChar;
        }
        public void addChildNode(Node childNode) {
            childNodes.add(childNode);
        }
    }

????需要注意的是,这个最小生成树不一定是常见的二叉树。每个节点的子节点数是不定的。

(5)Kruskal算法

????Kruskal算法也是一种找出最小生成树的算法。
????主要思想:依次选一条权值最小的边,如果没有产生回路,就加入到最小生成树中;如果产生回路,执行下一次循环;这个过程执行(n-1)次,就获得了需要的最小生成树。
????分析:可以看到,选择新的一条边时,是否形成了循环,就是关键所在;这里需要用到“不相交集”的概念,在开始时,将所有节点都看作互不相交的子集,每个节点的父节点是自身;如果从 i 到 j 有一条边,那么将 i 和 j合并成一个集合,并使得 i 是 j 的父亲节点;处理后续的边时,只要两节点的父节点不同,说明它们不在同一个集合里,就不会形成回路。
????Java实现:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

/**
 * 使用优先队列的Kruskal算法
 */
public class KruskalPQ {

    //一般的图结构
    class Graph {
        static final int NUM = 10; //图的最大顶点数
        char[] vertex = new char[NUM]; //顶点集合
        int vertexNum;//实际顶点数量
        int gType = 0; //0表示无向图,1表示有向图
        int[][] edgeWeight = new int[NUM][NUM]; //邻接矩阵,含权
        boolean[] isTrav = new boolean[NUM]; //是否访问的标志
    }

    /**
     * 图中的边
     */
    class Edge {
        int srcIndex; //起始位置
        int endIndex; //结束位置
        int weight; //权值
    }

    class Subset { // 不相交集
        int fatherIndex;//父节点的下标
        int rank; //合并两个不相交集时的依据
    }

    PriorityQueue<Edge> priorityQueue;

    public KruskalPQ() {
        priorityQueue = new PriorityQueue<>((o1, o2) -> o1.weight - o2.weight);
    }

    /**
     * 找到index= i的节点的父节点
     *
     * @param subsets
     * @param i
     * @return
     */
    int find(Subset[] subsets, int i) {
        if (subsets[i].fatherIndex != i) { //不相等,说明当前节点设置了父节点,继续查找父节点
            subsets[i].fatherIndex = find(subsets, subsets[i].fatherIndex);
            return subsets[i].fatherIndex;
        } else {
            return i; //相等,返回自身
        }
    }

    /**
     * 将x、y节点合并到一个集合,使得它们的fatherIndex相同
     *
     * @param subsets
     * @param x
     * @param y
     */
    void union(Subset[] subsets, int x, int y) {
        int xRoot = find(subsets, x);
        int yRoot = find(subsets, y);

        if (subsets[xRoot].rank < subsets[yRoot].rank) {
            subsets[xRoot].fatherIndex = yRoot;
        } else if (subsets[xRoot].rank > subsets[yRoot].rank) {
            subsets[yRoot].fatherIndex = xRoot;
        } else { //如果rank相等,使其中一个成为root,rank加1
            subsets[yRoot].fatherIndex = xRoot;
            subsets[xRoot].rank++;
        }
    }

    Graph kruskal(Graph g) {
        Graph resultG = new Graph(); //目标最小生成树,用图结构表示
        resultG.vertex = g.vertex; //最终顶点是一样的
        resultG.vertexNum = g.vertexNum; //最终顶点数量也是一样的

        addAllEdges(g); //添加所有的边

        Subset[] subsets = new Subset[g.vertexNum];
        for (int i = 0; i < subsets.length; i++) {
            Subset tmpSubset = new Subset();
            tmpSubset.fatherIndex = i; //初始时,fatherIndex设置为自己
            tmpSubset.rank = 0;
            subsets[i] = tmpSubset;
        }

        List<Edge> selectedEdgeList = new ArrayList<>();//最小生成树选择的边

        for (int i = 0; i < g.vertexNum - 1; i++) {
            Edge smallEdge = priorityQueue.remove(); //获得权值最小的一条边
            int x = find(subsets, smallEdge.srcIndex);
            int y = find(subsets, smallEdge.endIndex);
            if (x != y) {//不相等,说明不存在环
                selectedEdgeList.add(smallEdge);
                resultG.edgeWeight[smallEdge.srcIndex][smallEdge.endIndex] = smallEdge.weight;
                union(subsets, x, y);
            }
        }

        System.out.println("(边)顶点对\t\t\t\t权值");
        for (Edge edge : selectedEdgeList) {
            System.out.println(g.vertex[edge.srcIndex] + " <------> " + g.vertex[edge.endIndex] + "\t\t" + g.edgeWeight[edge.srcIndex][edge.endIndex]);
        }

        return resultG;
    }


    //将所有的边加入到PriorityQueue中
    private void addAllEdges(Graph g) {
        int limit = 0;

        for (int i = 0; i < g.vertexNum-1; i++) {
            if (g.gType == 0) { //无向图,只处理一半
                limit = i + 1;
            }
            for (int j = limit; j < g.vertexNum; j++) {
                if (g.edgeWeight[i][j] != 0) {
                    Edge edge = new Edge();
                    edge.weight = g.edgeWeight[i][j];
                    edge.srcIndex = i;
                    edge.endIndex = j;
                    priorityQueue.add(edge);
                }
            }
        }
    }

    /**
     * 构造一个图
     *
     * @return
     */
    Graph createGraph() {
        Graph graph = new Graph();
        graph.vertexNum = 6;
        graph.edgeWeight = new int[][]{
                {0, 1, 3, 0, 0, 0},
                {1, 0, 0, 4, 0, 0},
                {3, 0, 0, 0, 2, 0},
                {0, 4, 0, 0, 0, 10},
                {0, 0, 2, 0, 0, 9},
                {0, 0, 0, 10, 9, 0}};
        graph.vertex = new char[]{'A', 'B', 'C', 'D', 'E', 'F'};
        return graph;
    }

    public static void main(String[] args) {
        KruskalPQ kruskalPQ = new KruskalPQ();

        Graph graph = kruskalPQ.createGraph();
        Graph resultG = kruskalPQ.kruskal(graph);

        System.out.println("顶点对\t\t\t\t权值");
        for (int i = 0; i < resultG.vertexNum; i++) {
            for (int j = i + 1; j < resultG.vertexNum; j++) {
                if (resultG.edgeWeight[i][j] != 0) {
                    System.out.println(resultG.vertex[i] + " <------> " + resultG.vertex[j] + "\t\t" + resultG.edgeWeight[i][j]);
                }
            }
        }

    }
}

????输出:

(边)顶点对????????权值
A <------> B??????1
C <------> E??????2
A <------> C??????3
B <------> D??????4
E <------> F??????9
顶点对???????????权值
A <------> B??????1
A <------> C??????3
B <------> D??????4
C <------> E??????2
E <------> F??????9

????结果是一致,只是输出顺序不同而已。上面的依据每次选择的最小边输出,下面则是依据图的顶点顺序输出。
????此外,这里非常适合使用PriorityQueue,每次只需要选择权值最小的边,而不管这条边的顶点是否与已选顶点连通,这一点是与Prim算法非常不同的一个地方。如果不使用PriorityQueue,那么每次选最小边时,就需要重新排序了。
????Over !


https://www.xamrdz.com/backend/3yw1938975.html

相关文章: