第十二话 程序员常用的十种算法

12.1 二分查找算法(非递归)

12.1.1 算法的基本介绍

  1. 前面我们讲过了二分查找算法,是使用递归的方式,下面讲解的是二分查找算法的非递归方式
  2. 二分查找算法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
  3. 二分查找法的运行时间为对数时间O(nlogn),即查找到需要的目标位置最多只需要logn步

12.1.2 二分查找算法(非递归)代码实现

数组{1,3,8,10,11,67,100},编程实现二分查找,要求使用非递归的递归的方式完成。

public class BinarySearchNoRecur {
    public static void main(String[] args) {
        int[] arr = {1,3,8,10,11,67,100};
        int index = binarySearch(arr,100);
        System.out.println("index = "+index);
    }
    //二分查找的非递归实现
    /**
     *
     * @param arr 待查找的数组,arr是升序排序
     * @param target 需要查找的数
     * @return 返回对应的下标,-1表示没有找到
     */
    public static int binarySearch(int[] arr,int target){
        int left = 0;
        int right = arr.length - 1;
        while (left <= right){
            //满足条件继续查找
            int mid = (left + right)/2;
            if (arr[mid] == target){
                return mid;
            } else if (arr[mid] > target){
                right = mid - 1; //需要向做查找
            } else {
                left = mid + 1;  //需要向右查找
            }
        }
        return -1;
    }
}

结果的实现

index = 6

12.2 分治算法

12.2.1 基本介绍

  1. 分治算法是一种很重要的算法,字面上的解释是:分而治之,就是把一个复杂的问题分成两个或者很多的相同或者相似的子问题,再把子问题分成更小的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,

  2. 分治算法可以求解一些经典问题

    • 二分搜索

    • 大整数乘法

    • 合并问题

    • 快速排序

    • 线性时间选择

    • 最接近点对问题

    • 循环赛日程表

    • 汉诺塔

分治算法的基本步骤

  1. 分解:将原有问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  2. 解决:若子问题规模较小而容易被解决则直接解,否则递归的解决各个子问题
  3. 合并:将各个子问题的解合并为原问题的解。

分治算法设计模式

if |P| <= n0
    then return(ADHOC(P));
//将问题分解为较小的子问题P1,P2,P3,....,Pk
for i<--1 to k
    do yi<--Divide-and-Conquer(pi) 递归解决Pi
        T<--MERGE(y1,y2,...,yk)合并子问题
return(T)

其中|P|表示问题P的规模:n0为一般阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解,ADHOC§是该分治算法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时,直接用算法ADHOC§求解,算法MERGER(y1,y2,…,yk)是该分治算法中的合并子算法,用于直接将P的子问题P1,P2,的相应的解y1,y2合并为P的解。

12.2.2 分治算法的经典案例

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

思路分析

  1. 如果只有一个盘,直接A------>C

  2. 如果有2个及以上的盘子,我们总是可以看做是两个盘子,1.最下边的盘子,2.上面的盘子

    2.1 先把最上面的盘A—>B

    2.2 把最下面的盘A—>C

    2.3 把B塔所有的盘从B—>C

代码

public class Hanoitower {
    public static void main(String[] args) {
        hanoiTower(5,'A','B','C');

    }
    //汉诺塔的移动方法
    //使用分治算法

    public static void hanoiTower(int num, char a, char b, char c) {
        //如果只有一个盘
        if (num == 1) {
            System.out.println("第一个盘从 " + a + "==>" + c);
        } else {
            //如果我们有n >= 2情况,我们总是可以看做是两个盘1. 最下边的一个盘子。2. 上面的所有盘子
            //1. 先把上面的所有盘A==>B,移动过程中会使用到C
            hanoiTower(num - 1, a, c, b);
            //2. 把最下边的盘A==>C
            System.out.println("第" + num + "个盘从 " + a + "==>" + c);
            //3. 把B塔的所有盘从B==>C,移动过程中使用到A塔
            hanoiTower(num - 1, b, a, c);
        }
    }
}

结果

第一个盘从 A==>C
第2个盘从 A==>B
第一个盘从 C==>B
第3个盘从 A==>C
第一个盘从 B==>A
第2个盘从 B==>C
第一个盘从 A==>C
第4个盘从 A==>B
第一个盘从 C==>B
第2个盘从 C==>A
第一个盘从 B==>A
第3个盘从 C==>B
第一个盘从 A==>C
第2个盘从 A==>B
第一个盘从 C==>B
第5个盘从 A==>C
第一个盘从 B==>A
第2个盘从 B==>C
第一个盘从 A==>C
第3个盘从 B==>A
第一个盘从 C==>B
第2个盘从 C==>A
第一个盘从 B==>A
第4个盘从 B==>C
第一个盘从 A==>C
第2个盘从 A==>B
第一个盘从 C==>B
第3个盘从 A==>C
第一个盘从 B==>A
第2个盘从 B==>C
第一个盘从 A==>C

12.3 动态规划算法

12.3.1 动态规划算法的基本介绍

  1. 动态规划(Dynamic Programming)算法的核心思想是:将大的问题划分为小问题解决,从而进一步获取最优解的处理算法
  2. 动态规划算法与分治算法类似,其基本思想也是将待求解的算法分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解
  3. 与分治算法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往是相互独立的。(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行下一步的求解)。
  4. 动态规划可以通过填表的方式来逐步推进,得到最优解。

12.3.2 案例

背包问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQdmPONT-1619503735764)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210412190953332.png)]

思路分析

  1. 背包问题主要是指一个给定容量的背包,若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大,其中又分为01背包和完全背包(01背包指的是放入的东西不可以重复,完全背包指的是:每种物品都有无限件可以使用)
  2. 这个问题属于01背包,即每个物品最多可以放一个,而无限背包可以转换为01背包。
  3. 算法的主要思想:利用动态规划来解决,每次遍历到第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入到背包,即对于给定的n个物品,设v[i]和w[i]分别为物品的价值和重量,C为背包的容量,再令v[i] [j]表示在前i个物品中能够装入容量为j的背包中的最大价值,

几个重要的代码讲解

  1. v[i] [0] = v[0] [j] = 0; //表示填入表的第一行和第一列都为零
  2. 当w[j] > j时:v[i] [j] = v[i-1] [j] //当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略。
  3. 当j >= w[i]时:v[i] [j] = max{v[i-1] [j],v[i] + v[i-1] [i-w[i]]},//当准备加入的新增的商品的容量小于等于当前背包的容量,//装入的方式:v[i-1] [j]:就是上一个单元格的装入的最大值,v[i]表示当前商品的价值,v[i-1] [j-w[i]]:装入i-1商品,到剩余空间j-w[i]的最大值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PNjhfFcg-1619503735770)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210412201228126.png)]

public class KnapsackProblem {
    public static void main(String[] args) {
        int[] w = {1, 4, 3};  //物品的重量
        int[] val = {1500, 3000, 2000};//物品的价值,这里val[i]就是前面讲的v[i]
        int m = 4; //背包的容量
        int n = val.length; //物品的个数

        //创建二维数组
        //v[i][j] 表示在前i个物品中能够装入容量为j的背包的最大价值
        int[][] v = new int[n + 1][m + 1];
        //为了记录放入商品的情况,我们来制定一个二维数组
        int[][] path = new int[n+1][m+1];

        //首先是初始化第一行和第一列,这里在本程序中,可以不去处理,因为默认的是零
        for (int i = 0; i < v.length; i++) {
            v[i][0] = 0;
        }
        for (int i = 0; i < v[0].length; i++) {
            v[0][i] = 0;
        }

        //根据前面得到的公式来动态规划处理
        for (int i = 1; i < v.length; i++) {
            //不处理第一行 i 是从1开始的
            for (int j = 1; j < v[0].length; j++) {
                if (w[i - 1] > j) {
                    v[i][j] = v[i - 1][j];
                } else {
//                    v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
                    //为了记录商品存放到背包的情况,我们不能直接的使用上面的公式,需要使用if-else来体现出公式
                    if (v[i-1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]){
                        v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
                        path[i][j] = 1;
                    } else {
                        v[i][j] = v[i - 1][j];
                    }
                }
            }
        }

        //输出一下V看看目前的情况
        for (int i = 0; i < v.length; i++) {
            for (int j = 0; j < v[i].length; j++) {
                System.out.print(v[i][j] + " ");
            }
            System.out.println();
        }

        //需要认真思考的部分
        int i = path.length -1; //行的最大下标
        int j = path[0].length - 1;//列的最大下标
        while (i > 0 && j > 0){
            if (path[i][j] == 1) {
                System.out.printf("第%d个商品放入到背包\n", i);
                j -= w[i - 1];
            }
            i--;
        }

    }
}

结果

0 0 0 0 0 
0 1500 1500 1500 1500 
0 1500 1500 1500 3000 
0 1500 1500 2000 3500 
第3个商品放入到背包
第1个商品放入到背包

12.4 KMP算法

应用场景—字符串匹配问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVs2nUK9-1619503735776)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413092653533.png)]

12.4.1 暴力匹配算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpOAu9Yn-1619503735780)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413093130622.png)]

代码

public class ViolenceMatch {
    public static void main(String[] args) {
        //测试暴力匹配算法
        String str1 = "这个程序你都不会,你可真行呀";
        String str2 = "都不会";

        int index = violenceMatch(str1,str2);
        System.out.println("index = "+index);
    }
    //暴力匹配算法实现
    public static int violenceMatch(String str1, String str2){
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();

        int s1Len = s1.length;
        int s2Len = s2.length;

        int i = 0;  //i的索引指向s1
        int j = 0;  //j的索引指向s2
        while (i < s1Len && j < s2Len){
            if (s1[i] == s2[j]){
                //匹配OK
                i++;
                j++;
            } else {
                //如果匹配失败的话
                i = i - (j - 1);
                j = 0;
            }
        }
        //判断是否匹配成功
        if (j == s2Len){
            return i - j;
        } else {
            return -1;
        }
    }
}

结果

index = 5

12.4.2 KMP算法

基本介绍

  1. KMP算法是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
  2. Knuth-Morris-Pratt字符串查找算法,简称KMP算法,常用在一个文本串S内查找一个模式串P的出现位置,
  3. KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。

部分匹配值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F8Shmvju-1619503735794)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413103416126.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tzn1UM4A-1619503735795)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413103434122.png)]

部分匹配的实质是,有时候,字符串头部和尾部会有重复,比如“ABCDAB"之中有两个”AB“,那么它的部分匹配值就是2。搜索词移动的时候,第一个”AB“向后移动4位(字符串长度-部分匹配值),这样就可以来到第二个AB的位置。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iRmJAfDl-1619503735797)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413103942370.png)]

代码

public class KMPAlgorithm {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";

        int[] next = kmpNext("ABCDABD");
        System.out.println("next = "+ Arrays.toString(next));

        int index = kmpSearch(str1,str2,next);
        System.out.println("index = "+index);
    }

    //写出我们的kmp搜索算法

    /**
     *
     * @param str1  源字符串
     * @param str2  字串
     * @param next  部分匹配表,是字串对应的部分匹配表
     * @return 如果是-1就是没有匹配到,否则就返回第一个匹配的位置
     */
    public static int kmpSearch(String str1, String str2,int[] next){
        //遍历
        for (int i = 0, j = 0;i < str1.length(); i++){

            //需要处理不相等的情况
            //KMP的核心算法
            while (j > 0 && str1.charAt(i) != str2.charAt(j)){
                j = next[j - 1];
            }
            if (str1.charAt(i) == str2.charAt(j)){
                j++;
            }
            if (j == str2.length()){
                //找到了
                return i - j + 1;
            }
        }
        return -1;
    }


    //获取到一个字符串(字串)的部分匹配值表
    public static int[] kmpNext(String dest){
        //创建一个next数组保存部分匹配值
        int[] next = new int[dest.length()];
        next[0] = 0; //如果字符串是长度为1部分匹配值就是0
        for (int i = 1, j = 0; i < dest.length(); i++){
            //当dest.charAt(i) != dest.charAt(j),我们需要从next[ j -1 ]获取新的j
            //直到我们发现等式成立才退出
            //这个是KMP的核心算法
            while (j > 0 && dest.charAt(i) != dest.charAt(j)){
                j = next[j - 1];
            }
            //当满足dest.charAt(i) == dest.charAt(j)时,部分匹配值就是加1
            if (dest.charAt(i) == dest.charAt(j)){
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

运行结果

next = [0, 0, 0, 0, 1, 2, 0]
index = 15

12.5 贪心算法

12.5.1 贪心算法的基本介绍

介绍

  1. 贪心算法又叫做贪婪算法,是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
  2. 贪婪算法所得到的结果**不一定是最优的结果(有时候会是最优解),**但是都是相近相似(接近)最优解的结果。

12.5.2 案例

假设存在如下表需要付费的广播台,以及广播台可以覆盖的地区,如何选择最少的广播台,让所有的地区都可以接收到信号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcaBa6gu-1619503735798)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413135629323.png)]

解决思路

目前没有算法可以快速计算得到准备的值,使用贪婪算法,则可以得到非常接近的解,并且效率很高,选择策略上,因为需要覆盖全部的地区的最小集合:

  1. 遍历所有的广播电台,找到一个覆盖了最多未覆盖的地区的电台(此电台可能含有一些已覆盖的地区,但是没有关系)
  2. 将这个电台加入到一个集合中(比如ArrayList),想办法把该电台覆盖的地区在下次比较的时候去掉
  3. 重复第一步直到覆盖了全部的地区
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

public class GreedyAlgorithm {
    public static void main(String[] args) {

        //创建广播电台放到Map
        HashMap<String, HashSet<String>> broadcasts = new HashMap<String,HashSet<String>>();
        //将各个电台放入到broacasts
        HashSet<String> hashSet1 = new HashSet<String>();
        hashSet1.add("北京");
        hashSet1.add("上海");
        hashSet1.add("天津");

        HashSet<String> hashSet2 = new HashSet<String>();
        hashSet2.add("广州");
        hashSet2.add("北京");
        hashSet2.add("深圳");

        HashSet<String> hashSet3 = new HashSet<String>();
        hashSet3.add("成都");
        hashSet3.add("上海");
        hashSet3.add("杭州");

        HashSet<String> hashSet4 = new HashSet<String>();
        hashSet3.add("上海");
        hashSet3.add("天津");

        HashSet<String> hashSet5 = new HashSet<String>();
        hashSet5.add("杭州");
        hashSet5.add("大连");

        //加入到map
        broadcasts.put("k1",hashSet1);
        broadcasts.put("k2",hashSet2);
        broadcasts.put("k3",hashSet3);
        broadcasts.put("k4",hashSet4);
        broadcasts.put("k5",hashSet5);

        //allAreas存放所有的地区
        HashSet<String> allAreas = new HashSet<String>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");

         //创建ArrayList ,存放选择的电台集合
        ArrayList<String> selects = new ArrayList<String>();

        //定义一个临时的集合,在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
        HashSet<String> tempSet = new HashSet<String>();

        //定义一个maxKey 保存一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key
        //如果maxKey不为null,则会加入到selects
        String maxKey = null;
        while (allAreas.size() != 0){//如果allAreas不为0,则表示还没有覆盖到所有的地区
            //每进行一次while,需要
            maxKey = null;

            //遍历broadcasts,取出对应的key
            for (String key:broadcasts.keySet()){
                //每进行一次for循环
                tempSet.clear();
                //当这个key能够覆盖的地区
                HashSet<String> areas = broadcasts.get(key);
                tempSet.addAll(areas);
                //求出tempSet和allAreas 集合的交集,交集会赋给tempSet
                tempSet.retainAll(allAreas);
                //如果当前这个集合包含的未覆盖地区的数量,比maxKey指向的集合地区还多
                //就需要重置maxKey
                //tempSet.size() > broadcasts.get(maxKey).size()体现出贪心算法的特点,每次都选择最优的。
                if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())){
                    maxKey = key;
                }
            }
            //maxKey != null; 就应该将maxKey 加入 selects
            if (maxKey != null){
                selects.add(maxKey);
                //将maxKey指向的广播电台覆盖的地区,从allAreas去掉
                allAreas.removeAll(broadcasts.get(maxKey));
            }

        }
        System.out.println("得到的选择结果"+selects);
    }
}
得到的选择结果[k3, k1, k2, k5]

12.6 普里姆算法

应用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5kSyGyYp-1619503735801)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413194558324.png)]

  1. 有胜利乡有7个村庄(A,B,C,D,E,F,G),现在需要修路把7个村庄连通
  2. 各个村庄的距离用边线表示(权),比如A-B距离5公里。
  3. 问:如何修路保证各个村庄能连通,并且总的修建公路总里程最短?

最小生成树

修路问题的本质就是最小生成树问题,先介绍一下最小生成树(Minimum Cost Spanning Tree)简称MST

  1. 给定一个带权的无向连通图,如何选取一棵生成树,使书上所有边上权的总和最小,这就叫做最小生成树
  2. N个顶点,一定有N-1条边
  3. 包含全部顶点
  4. N-1条边都在图中
  5. 求最小生成树的算法主要是普利姆算法和克鲁斯卡尔算法

12.6.1 算法介绍

基本介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rU47Ge37-1619503735803)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413195340667.png)]

图解分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WMmaOqTu-1619503735804)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413201036629.png)]

12.6.2 代码实现

public class PrimAlgorithm {
    public static void main(String[] args) {
        //测试看看图是否创建OK
        char[] data = new char[]{'A','B','C','D','E','F','G'};
        int verxs = data.length;
        //邻接矩阵的关系使用二维数组表示,10000这个树表示两个点是不连通的
        int[][] weight = new int[][]{
                {10000,5,7,10000,10000,10000,2},
                {5,10000,10000,9,10000,10000,3},
                {7,10000,10000,10000,8,10000,10000},
                {10000,9,10000,10000,10000,4,10000},
                {10000,10000,8,10000,10000,5,4},
                {10000,10000,10000,4,5,10000,6},
                {2,3,10000,10000,4,6,10000}
        };
        //创建MGraph对象
        MGraph graph = new MGraph(verxs);
        //创建一个minTree对象
        MinTree minTree = new MinTree();
        minTree.creatGraph(graph,verxs,data,weight);
        //输出
        minTree.showGraph(graph);
        //测试普利姆算法
        minTree.prim(graph,0);
    }
}

//创建最小生成树==>村庄的图
class MinTree{
    //创建图的邻接矩阵

    /**
     *
     * @param graph 图对象
     * @param verxs 图对应的顶点个数
     * @param data 图的各个顶点的值
     * @param weight 图的邻接矩阵
     */
    public void creatGraph(MGraph graph,int verxs, char[] data, int [][] weight){
        int i, j;
        for (i = 0;i<verxs;i++){
            graph.data[i] = data[i];
            for (j = 0; j < verxs; j++){
                graph.weight[i][j] = weight[i][j];
            }
        }
    }
    //显示图的邻接矩阵
    public void showGraph(MGraph graph){
        for (int[] link:graph.weight){
            System.out.println(Arrays.toString(link));
        }
    }
    //编写prim算法,得到最小生成树

    public void prim(MGraph graph,int v){
        //visited[] 标记节点(顶点)是否被访问过
        int[] visited = new int[graph.verxs];
        //把当前这个节点标记为已访问
        visited[v] = 1;
        //h1 和 h2记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = 10000;  //将minWeight 初始化成一个很大的值,后面遍历的时候会被替换
        for (int k = 1; k < graph.verxs; k++){
            //因为有n个顶点所以我们只需要n-1条边就可以了
            //这个是确定每一次生成的子图,和哪个节点的距离最近
            for (int i = 0; i<graph.verxs; i++){
                //i节点表示被访问过的节点
                for (int j = 0; j < graph.verxs; j++){
                    //j节点表示还没有访问过的节点
                    if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){
                        //替换minWeight(寻找已经访问过的节点和未访问过的节点间的权值最小边)
                        minWeight = graph.weight[i][j];
                        h1 = i;
                        h2 = j;
                    }
                }
            }
            //找到一条边最小
            System.out.println("边<"+graph.data[h1]+","+graph.data[h2]+"> 权值:"+minWeight);
            //将当前这个节点标记未已经访问
            visited[h2] = 1;
            //minWeight 重新设置为最大值10000
            minWeight = 10000;
        }
    }
}

class MGraph{
    int verxs;  //表示图的节点个数
    char[] data;  //存放节点数据
    int[][] weight;  //存放边,就是我们的邻接矩阵

    public MGraph(int verxs){
        this.verxs = verxs;
        data = new char[verxs];
        weight = new int[verxs][verxs];
    }
}
[10000, 5, 7, 10000, 10000, 10000, 2]
[5, 10000, 10000, 9, 10000, 10000, 3]
[7, 10000, 10000, 10000, 8, 10000, 10000]
[10000, 9, 10000, 10000, 10000, 4, 10000]
[10000, 10000, 8, 10000, 10000, 5, 4]
[10000, 10000, 10000, 4, 5, 10000, 6]
[2, 3, 10000, 10000, 4, 6, 10000]
边<A,G> 权值:2
边<G,B> 权值:3
边<G,E> 权值:4
边<E,F> 权值:5
边<F,D> 权值:4
边<A,C> 权值:7

12.7 克鲁斯卡尔算法

应用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6QrxR9pG-1619503735807)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210414092032042.png)]

  1. 某城市新增7个站点,现在需要修路把七个站点连通
  2. 各个站点的距离用边线表示(权),比如A-B距离12公里
  3. 问:如何修路保证各个站点都能连通,并且总的修建公里总路程最短

12.7.1 算法的基本介绍

介绍

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

图解分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S6jfwECY-1619503735809)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210414093423761.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGREqas1-1619503735810)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210414093634959.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FqsfUkLM-1619503735812)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210414095300212.png)]

12.7.2 算法的实现

public class KruskalCase {

    private int edgNum;  //边的个数
    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[][] = {
                {0, 12, INF, INF, INF, 16, 14},
                {12, 0, 10, INF, INF, 7, INF},
                {INF, 10, 0, 3, 5, 6, INF},
                {INF, INF, 3, 0, 4, INF, INF},
                {INF, INF, 5, 4, 0, 2, 8},
                {16, 7, 6, INF, 2, 0, 9},
                {14, INF, INF, INF, 8, 9, 0}
        };
        //创建KruskalCase对象实例
        KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
        //输出创建
        kruskalCase.print();
        kruskalCase.kruskal();
    }

    //构造器
    public KruskalCase(char[] vertexs, int[][] matrix) {
        //初始化定点数和边的个数
        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) {
                    edgNum++;
                }
            }
        }
    }

    public void kruskal(){
        int index = 0;  //表示最后结果数组的索引
        int[] ends = new int[edgNum];  //用于保存“已有最小生成树”中的每个顶点在最小生成树的终点
        //创建结果数组,保存最后的最小生成树
        EData[] rets = new EData[edgNum];

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

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

        //遍历edges 数组,将边添加到最小生成树中时,判断准备加入的边是否形成了回路,如果没有,就加入rets,否则不能加入
        for (int i = 0; i <edgNum; 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;  //设置m在"已有最小生成树“中的终点
                rets[index++] = edges[i];  //有一条边加入到rets数组
            }
        }
        //统计并打印:”最小生成树“,输出rets
        System.out.println("最小生成树为");
        for (int i = 0; i < index; i++){
            System.out.println(rets[i]);
        }
    }

    //打印邻接矩阵
    public void print() {
        System.out.println("邻接矩阵为: \n");
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = 0; j < vertexs.length; j++) {
                System.out.printf("%15d", matrix[i][j]);
            }
            System.out.println();
        }
    }

    /**
     * 功能:对边进行排序,冒泡排序
     *
     * @param edges 边的合集
     */
    private void sortEdges(EData[] edges) {
        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) {
                    //交换
                    EData temp = edges[j];
                    edges[j] = edges[j + 1];
                    edges[j + 1] = temp;
                }
            }
        }
    }

    /**
     * @param ch 顶点的值,比如'A','B'
     * @return 返回ch顶点对应的下标,如果找不到,就返回-1
     */
    private int getPosition(char ch) {
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == ch) {
                return i;
            }
        }
        //找不到就返回-1
        return -1;
    }

    /**
     * 功能:获取图中边,放到EData[] 数组里面,后面我们需要遍历该数组,是通过matrix邻接矩阵来获取
     * EData[] 形式[[‘A’,'B',12],['B','F',7],....]
     *
     * @return
     */
    private EData[] getEdges() {
        int index = 0;
        EData[] edges = new EData[edgNum];
        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;
    }

    /**
     * 功能:获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
     * @param ends 数组就是记录了各个顶点对应的终点是哪个,ends数组是在遍历过程中,逐步形成
     * @param i 表示传入的顶点对应的下标
     * @return 返回的就是下标为i的这个顶点对应的终点的下标,
     */
    private int getEnd(int[] ends, int i){
        while (ends[i] != 0){
            i = ends[i];
        }
        return i;
    }
}

//创建一个类EData,它们对象实例就表示一个边
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;
    }

    //重写toString,便于输出边信息
    @Override
    public String toString() {
        return "EData [<" + start + ", " + end + "> ===>" + 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
图的边的集合[EData [<A, B> ===>12], EData [<A, F> ===>16], EData [<A, G> ===>14], EData [<B, C> ===>10], EData [<B, F> ===>7], EData [<C, D> ===>3], EData [<C, E> ===>5], EData [<C, F> ===>6], EData [<D, E> ===>4], EData [<E, F> ===>2], EData [<E, G> ===>8], EData [<F, G> ===>9]]共12条边
最小生成树为
EData [<E, F> ===>2]
EData [<C, D> ===>3]
EData [<D, E> ===>4]
EData [<B, F> ===>7]
EData [<E, G> ===>8]
EData [<A, B> ===>12]

12.8 迪杰斯特拉算法

案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dnqKmRmn-1619503735814)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210413194558324.png)]

  1. 战争时期,胜利乡有7个村庄,现在又六个邮差,从G点出发,,需要分别把邮件送到ABCDEF六个村庄
  2. 各个村庄的距离用边线表示,
  3. 问:如何计算出G村庄到其他各个村庄的最短距离。
  4. 如果从其他点出发到各个点的最短距离又是多少呢?

12.8.1 迪杰斯特拉算法的介绍

介绍

迪杰斯特拉算法是典型最短路劲算法,用于计算一个节点到其他节点的最短路径,它的主要特点是以起点为中心向外层扩展(广度优先搜索算法),直到扩展到终点为止。

算法过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rGYU2SI-1619503735815)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210414165541324.png)]

12.8.2 算法

public class DijkstraAlgorithm {
    public static void main(String[] args) {
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;  //表示不可连接
        matrix[0] = new int[]{N, 5, 7, N, N, N, 2};
        matrix[1] = new int[]{5, N, N, 9, N, N, 3};
        matrix[2] = new int[]{7, N, N, N, 8, N, N};
        matrix[3] = new int[]{N, 9, N, N, N, 4, N};
        matrix[4] = new int[]{N, N, 8, N, N, 5, 4};
        matrix[5] = new int[]{N, N, N, 4, 5, N, 6};
        matrix[6] = new int[]{2, 3, N, N, 4, 6, N};
        //创建Graph对象
        Graph graph = new Graph(vertex, matrix);
        //测试
        graph.showGraph();
        graph.dsj(6);
        graph.showDijkstra();
    }
}

class Graph {
    private char[] vertex;  //顶点数组
    private int[][] matrix;  //邻接矩阵
    private VisitedVertex vv; //已经访问的顶点

    //构造器
    public Graph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }
    //显示结果
    public void showDijkstra(){
        vv.show();
    }

    //显示图
    public void showGraph() {
        for (int[] link : matrix) {
            System.out.println(Arrays.toString(link));
        }
    }
    //迪杰斯特拉算法的实现

    /**
     *
     * @param index 表示出发顶点对应的下标
     */
    public void dsj(int index){
        vv = new VisitedVertex(vertex.length,index);
        update(index); //跟新index顶点到周围顶点的距离和前驱顶点
        for (int j = 1; j < vertex.length; j++){
            index = vv.updateArr(); //选择并返回新的访问节点
            update(index);
        }
    }

    //更新index下标顶点到周围顶点的距离和周围顶点的前驱节点
    private void update(int index){
        int len = 0;
        //根据遍历我们的邻接矩阵的matrix[index]行
        for (int j = 0; j < matrix[index].length; j++){
            //len的含义是出发顶点到index的距离+从index顶点到j顶点的距离之和
            len = vv.getDis(index) + matrix[index][j];
            //如果j顶点没有被访问过,并且len小于出发顶点到j顶点的距离就需要更新
            if (!vv.in(j) && len < vv.getDis(j)){
                vv.updateDis(j,len);
                vv.updatePre(j,index);
            }
        }
    }
}

//已访问过顶点集合
class VisitedVertex {
    //记录各个顶点是否访问过:1表示访问过,0表示未访问,会动态更新
    public int[] already_arr;
    //每个下标对应的值为前一个顶点下标,会动态更新
    public int[] pre_visited;
    //记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其他顶点的记录,会动态更新,求最短距离就会放到dis
    public int[] dis;

    //构造器

    /**
     * @param length :
     * @param index
     */
    public VisitedVertex(int length, int index) {
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];

        //初始化dis数组
        Arrays.fill(dis, 65535);
        this.already_arr[index] = 1;
        this.dis[index] = 0; //设置出发顶点的访问距离为0
    }

    /**
     * 功能:判断Index顶点是否被访问过
     * @param index
     * @return 如果访问过就返回true,否则的话返回false
     */
    public boolean in(int index){
        return already_arr[index] == 1;
    }

    /**
     * 功能:更新出发顶点到Index顶点的距离
     * @param index
     * @param len
     */
    public void updateDis(int index, int len){
        dis[index] = len;
    }

    /**
     * 功能:更新pre这个顶点的前驱顶点为index顶点
     * @param pre
     * @param index
     */
    public void updatePre(int pre, int index){
        pre_visited[pre] = index;
    }

    /**
     * 功能:返回出发顶点到Index顶点的距离
     * @param index
     * @return
     */
    public int getDis(int index){
        return dis[index];
    }

    /**
     * 继续选择并返回新的访问顶点,比如这里的G完后,就是A点作为新的访问顶点(注意不是出发点)
     * @return
     */
    public int updateArr(){
        int min = 65535, index = 0;
        for (int i = 0; i < already_arr.length; i++){
            if (already_arr[i] == 0 && dis[i] < min){
                min = dis[i];
                index = i;
            }
        }
        //更新Index顶点被访问过
        already_arr[index] = 1;
        return index;
    }

    //显示最后的结果,就是将三个数组的情况输出
    public void show(){
        System.out.println("=================================");
        //输出already_arr
        for (int i : already_arr){
            System.out.print(i + " ");
        }
        System.out.println();
        //输出pre_visited
        for (int i : pre_visited){
            System.out.print(i + " ");
        }
        System.out.println();
        //输出dis
        for (int i : dis){
            System.out.print(i + " ");
        }
        System.out.println();

        //为了好看最后的最短距离
        char[] vertex = {'A','B','C','D','E','F','G'};
        int count = 0;
        for (int i : dis){
            if (i != 65535){
                System.out.print(vertex[count]+"{"+i+"}");
            } else {
                System.out.println("N");
            }
            count++;
        }
        System.out.println();
    }
}
[65535, 5, 7, 65535, 65535, 65535, 2]
[5, 65535, 65535, 9, 65535, 65535, 3]
[7, 65535, 65535, 65535, 8, 65535, 65535]
[65535, 9, 65535, 65535, 65535, 4, 65535]
[65535, 65535, 8, 65535, 65535, 5, 4]
[65535, 65535, 65535, 4, 5, 65535, 6]
[2, 3, 65535, 65535, 4, 6, 65535]
=================================
1 1 1 1 1 1 1 
6 6 0 5 6 6 0 
2 3 9 10 4 6 0 
A{2}B{3}C{9}D{10}E{4}F{6}G{0}

12.9 弗洛伊德算法

12.9.1 算法介绍

基本介绍

  1. 和Dijkstra算法一样,弗洛伊德算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。
  2. 弗洛伊德算法计算图中各个顶点之间的最短距离
  3. 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短距离
  4. 弗洛伊德算法VS迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问的点,求出从出发访问顶点到其他顶点的最短路径,弗洛伊德算法中的每一个顶点都是出发访问顶点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径

图解分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaxMU4Wy-1619503735817)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415110923017.png)]

参考这张图分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h0f7HwRa-1619503735819)(file://C:/Users/dongwei/AppData/Roaming/Typora/typora-user-images/image-20210413194558324.png?lastModify=1618455236)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RkUM94fq-1619503735820)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415121407511.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kbn8lv8R-1619503735824)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415121812213.png)]

将A作为中间顶点的情况有:

  1. C ==> A ==> G [9]
  2. C ==> A ==> B [12]
  3. G ==> A > B [7] 但是本身G>B是[3]所以不需要更新

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23Sf1o5q-1619503735826)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415121831552.png)]

算法的核心

中间顶点[A,B,C,D,E,F,G]

出发顶点[A,B,C,D,E,F,G]

终点 [A,B,C,D,E,F,G]

使用三层循环来更新距离表和前驱关系表,因此弗洛伊德算法的复杂度是n^3

12.9.2 算法的实现

public class FloydAlgorithm {
    public static void main(String[] args) {
        //测试图是否创建成功
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;  //表示不可连接
        matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
        matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
        matrix[2] = new int[]{7, N, 0, N, 8, N, N};
        matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
        matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
        matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
        matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};

        Graph graph = new Graph(vertex.length,matrix,vertex);

        graph.floyd();
        graph.show();
    }
}

//创建图
class Graph {
    private char[] vertex;  //存放顶点的数组
    private int[][] dis; //保存,从各个顶点出发到其他顶点的距离,最后的结果,也是保留在该数组
    private int[][] pre; //保存到达目的顶点的前驱顶点

    //构造器

    /**
     * @param length 大小
     * @param matrix 邻接矩阵
     * @param vertex 顶点数组
     */
    public Graph(int length, int[][] matrix, char[] vertex) {
        this.vertex = vertex;
        this.dis = matrix;
        this.pre = new int[length][length];
        //对pre数组初始化,注意放的是前驱顶点的下标
        for (int i = 0; i < length; i++) {
            Arrays.fill(pre[i], i);
        }
    }

    //显示pre数组和dis数组
    public void show() {
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        for (int k = 0; k < dis.length; k++) {
            //先将pre数组输出一行
            for (int i = 0; i < pre.length; i++) {
                System.out.print(vertex[pre[k][i]] + " ");
            }
            System.out.println();
            for (int i = 0; i < dis.length; i++) {
                System.out.print("("+vertex[k]+"==>"+vertex[i]+"的最短路径是"+dis[k][i] + ")  ");
            }
            System.out.println();
            System.out.println();
        }
    }

    //弗洛伊德算法
    public void floyd(){
        int len = 0; //变量保存距离
        //对中间顶点遍历,k 就是中间顶点的下标[A,B,C,D,E,F,G]
        for (int k = 0; k < dis.length; k++){
            for (int i = 0; i < dis.length; i++){
                for (int j = 0; j < dis.length; j++){
                    len = dis[i][k] + dis[k][j];  //==>求出从i顶点出发,经过k中间顶点,到达j顶点距离
                    if (len < dis[i][j]){
                        //如果len小于dis[i][j]
                        dis[i][j] = len;  //更新距离
                        pre[i][j] = pre[k][j];
                    }
                }
            }
        }
    }

}
A A A F G G A 
(A==>A的最短路径是0)  (A==>B的最短路径是5)  (A==>C的最短路径是7)  (A==>D的最短路径是12)  (A==>E的最短路径是6)  (A==>F的最短路径是8)  (A==>G的最短路径是2)  

B B A B G G B 
(B==>A的最短路径是5)  (B==>B的最短路径是0)  (B==>C的最短路径是12)  (B==>D的最短路径是9)  (B==>E的最短路径是7)  (B==>F的最短路径是9)  (B==>G的最短路径是3)  

C A C F C E A 
(C==>A的最短路径是7)  (C==>B的最短路径是12)  (C==>C的最短路径是0)  (C==>D的最短路径是17)  (C==>E的最短路径是8)  (C==>F的最短路径是13)  (C==>G的最短路径是9)  

G D E D F D F 
(D==>A的最短路径是12)  (D==>B的最短路径是9)  (D==>C的最短路径是17)  (D==>D的最短路径是0)  (D==>E的最短路径是9)  (D==>F的最短路径是4)  (D==>G的最短路径是10)  

G G E F E E E 
(E==>A的最短路径是6)  (E==>B的最短路径是7)  (E==>C的最短路径是8)  (E==>D的最短路径是9)  (E==>E的最短路径是0)  (E==>F的最短路径是5)  (E==>G的最短路径是4)  

G G E F F F F 
(F==>A的最短路径是8)  (F==>B的最短路径是9)  (F==>C的最短路径是13)  (F==>D的最短路径是4)  (F==>E的最短路径是5)  (F==>F的最短路径是0)  (F==>G的最短路径是6)  

G G A F G G G 
(G==>A的最短路径是2)  (G==>B的最短路径是3)  (G==>C的最短路径是9)  (G==>D的最短路径是10)  (G==>E的最短路径是4)  (G==>F的最短路径是6)  (G==>G的最短路径是0)

12.10 马踏棋盘算法

12.10.1 算法介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-79wNQZYo-1619503735829)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415143950639.png)]

图文讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BRJ6u0in-1619503735831)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415144124042.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SljOlxGV-1619503735834)(C:\Users\dongwei\AppData\Roaming\Typora\typora-user-images\image-20210415145038706.png)]

12.10.2 代码实现

public class HorseChessBoard {

    private static int X; //棋盘的列数
    private static int Y; //棋盘的行数
    //创建一个数组,标记棋盘的各个位置是否被访问过
    private static boolean visited[];
    //使用一个属性,标记是否棋盘所有的位置都被访问
    private static boolean finished;  //如果为true,表示成功

    public static void main(String[] args) {
        //测试
        X = 8;
        Y = 8;
        int row = 1;
        int column = 1;
        //创建棋盘
        int[][] chessboard = new int[X][Y];
        visited = new boolean[X*Y];
        //测试一下耗时
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard,row-1,column-1,1);
        long end = System.currentTimeMillis();
        System.out.println("共耗"+(end- start)+"毫秒");

        //输出棋盘的最后情况
        for (int[] rows:chessboard){
            for (int step:rows){
                System.out.print(step +"\t");
            }
            System.out.println();
        }

    }
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step){
        chessboard[row][column] = step;
        visited[row * X + column] = true;//标记该位置已经被访问
        ArrayList<Point> ps = next(new Point(column,row));
        //遍历ps
        while (!ps.isEmpty()){
            Point p = ps.remove(0); //取出下一个可以走的位置
            //判断该点是否已经够访问过
            if (!visited[p.y * X + p.x]){
                //说明还没有访问过
                traversalChessboard(chessboard,p.y,p.x,step+1);
            }
        }
        //判断马儿是否完成了任务,使用step和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置为0
        //说明:step < X*Y 成立的情况有两种
        //1. 棋盘到目前为止,仍然没有走完
        //2. 棋盘处于一个回溯的过程
        if (step < X*Y && !finished){
            chessboard[row][column] = 0;
            visited[row * X + column] = false;
        } else {
            finished = true;
        }
    }

    /**
     * 功能:根据当前位置(Point对象),计算马儿还能走那些位置(Point),并放入到一个集合中(ArrayList),最多有八个位置
     * @param curPoint
     * @return
     */
    public static ArrayList<Point> next(Point curPoint){
        //创建一个ArrayList
        ArrayList<Point> ps = new ArrayList<Point>();
        //创建一个Point
        Point p1 = new Point();
        //表示马儿可以走5这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0){
            ps.add(new Point(p1));
        }
        //表示马儿可以走6这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y -2) >= 0){
            ps.add(new Point(p1));
        }
        //表示马儿可以走7这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y -2) >= 0){
            ps.add(new Point(p1));
        }
        //表示马儿可以走0这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y -1) >= 0){
            ps.add(new Point(p1));
        }
        //表示马儿可以走1这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y +1) < Y){
            ps.add(new Point(p1));
        }
        //表示马儿可以走2这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y +2) < Y){
            ps.add(new Point(p1));
        }
        //表示马儿可以走3这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y +2) < Y){
            ps.add(new Point(p1));
        }
        //表示马儿可以走4这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y +1) < Y){
            ps.add(new Point(p1));
        }
        return ps;
    }
}
共耗103097毫秒
1	8	11	16	3	18	13	64	
10	27	2	7	12	15	4	19	
53	24	9	28	17	6	63	14	
26	39	52	23	62	29	20	5	
43	54	25	38	51	22	33	30	
40	57	42	61	32	35	48	21	
55	44	59	50	37	46	31	34	
58	41	56	45	60	49	36	47	

12.10.3 使用贪心算法优化

使用贪心算法对原来的算法优化

  1. 我们获取当前位置,可以走的下一个位置的集合。获取当前位置可以走的下一个位置的集合ArrayList ps = next(new Point(cloumn,row))
  2. 我们需要对ps中所有的Point的下一步的所有集合的数目,进行非递减排序就OK

9 , 7 , 6 , 5 , 4 , 3 , 2 , 1 //递减排序

1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 //递增排序

1 , 2 ,2 , 2 ,3 , 3 , 4 , 5 , 6 //非递减

9,7,6 , 6, 5 , 5, 3 , 2, 1 //非递增

//根据当前这一步的所有下一步的选择位置,进行非递减排序,减少回溯的次数
    public static void sort(ArrayList<Point> ps){
        ps.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                //获取01的下一步的所有位置个数
                int count1 = next(o1).size();
                //获取到02的下一步的所有位置个数
                int count2 = next(o2).size();
                if (count1 < count2){
                    return -1;
                } else if (count1 == count2){
                    return 0;
                } else {
                    return 1;
                }
            }
        });
    }
共耗56毫秒
1	16	37	32	3	18	47	22	
38	31	2	17	48	21	4	19	
15	36	49	54	33	64	23	46	
30	39	60	35	50	53	20	5	
61	14	55	52	63	34	45	24	
40	29	62	59	56	51	6	9	
13	58	27	42	11	8	25	44	
28	41	12	57	26	43	10	7	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值