常用十种算法「Java数据结构与算法学习笔记13」

🔥工程代码已上传至github:github.com/doublev2026...

二分查找算法(非递归)

算法介绍

  • 前面学习的二分查找算法,是使用递归的方式。二分查找算法还有非递归方式
  • 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
  • 二分查找法的运行时间为对数时间O(㏒₂n),即查找到需要的目标位置最多只需要㏒₂n步,假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)

算法代码实现

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

java 复制代码
package com.algorithrm.binarysearch;

public class BinarySearchNoRecursion {
    public static void main(String[] args) {
        int[] arr = {1, 3, 8, 10, 11, 67, 100};
        int index = binarySearch(arr, 100);
        System.out.println("查找结果索引 = " + 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) { // 继续查找
            System.out.println("查找一次");
            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;
    }
}

分治算法

算法介绍

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

这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)...等。

分治算法可以求解的一些经典问题:

  • 二分搜索
  • 大整数乘法
  • 棋盘覆盖
  • 合并排序
  • 快速排序
  • 线性时间选择
  • 最接近点对问题
  • 循环赛日程表
  • 汉诺塔

算法步骤

分治法在每一层递归上都有三个步骤:

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

算法设计模式

分治(Divide-and-Conquer(P))算法设计模式如下:

java 复制代码
if |P|≤n0
   then return(ADHOC(P))
//将P分解为较小的子问题 P1 ,P2 ,...,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 。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解 。算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法 ,用于将P的子问题P1 ,P2 ,...,Pk的相应的解y1,y2,...,yk合并为P的解

算法实践:汉诺塔

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

演示和思路分析:

代码实现:

java 复制代码
package com.algorithrm.dac;

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

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

动态规划算法

算法介绍

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

算法实践:背包问题

背有一个背包,容量为4磅,现有如下物品,并要求:

  1. 要求达到的目标为装入的背包的总价值最大,并且重量不超出
  2. 要求装入的物品不能重复
物品 重量 价格
吉他(G) 1 1500
音响(S) 4 3000
电脑(L) 3 2000

思路分析:

  • 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是每种物品都有无限件可用)
  • 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。
  • 算法的主要思想,利用动态规划来解决。**每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。**则我们有下面的结果:
java 复制代码
// 表示填入表第一行和第一列是0
(1) v[i][0]=v[0][j]=0;
// 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
(2) 当 w[i]>j 时:v[i][j]=v[i-1][j]
// 当准备加入的新增的商品的容量小于等于当前背包的容量,装入的方式:
(3) 当 w[i]<=j 时:v[i][j]=max{ v[i-1][j], v[i]+v[i-1][j-w[i]] }
    v[i-1][j]:就是上一个单元格的装入的最大值
    v[i] : 表示当前商品的价值
    v[i-1][j-w[i]] :装入i-1商品,到剩余空间j-w[i]的最大值 
物品 0 磅 1 磅 2 磅 3 磅 4 磅
0 0 0 0 0
吉他(G) 0 1500 G 1500 G 1500 G 1500 G
音响(S) 0 1500 G 1500 G 1500 G 3000 S
电脑(L) 0 1500 G 1500 G 2000 L 2000 L + 1500 G

v[3][4] w[3] = 3,j=4

v[i-1][j]=3000 v[i]+v[i-1][j-w[i]]=v[3] + v[2][4-3] = 2000+1500 max=3500

算法代码实现

java 复制代码
package com.algorithrm.dynamic;

public class KnapsackProblem {
    public static void main(String[] args) {
        int[] w = {1, 4, 3}; // 物品的重量
        int[] val = {1500, 3000, 2000}; // 物品的价值
        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];

        // 初始化第一行和第一列,填入表第一行和第一列是0
        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;
        }

        /**
         * 根据公式来动态规划处理
         * 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
         *      当 w[i]>j 时:v[i][j]=v[i-1][j]
         * 当准备加入的新增的商品的容量小于等于当前背包的容量,装入的方式:
         *      当 w[i]<=j 时:v[i][j]=max{ v[i-1][j], v[i]+v[i-1][j-w[i]] }
         */
        for (int i = 1; i < v.length; i++) { // 不处理第一行,i从1开始
            for (int j = 1; j < v[0].length; j++) { // 不处理第一列,j从1开始
                // 根据公式处理
                // 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
                if (w[i - 1] > j) { // 因为i是从1开始的,所以需要用 w[i-1] 来对比
                    v[i][j] = v[i - 1][j];
                }
                // 当准备加入的新增的商品的容量小于等于当前背包的容量,装入的方式:
                // v[i][j]=max{ v[i-1][j], val[i-1]+v[i-1][j-w[i-1]] }
                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
                        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] + "\t\t");
            }
            System.out.println();
        }
        System.out.println("============================");

        // 输出最后我们是放入的哪些商品
        // 遍历 path,这样输出会把所有的放入情况都得到,其实我们只需要最后的放入
//        for (int i = 0; i < path.length; i++) {
//            for (int j = 0; j < path[i].length; j++) {
//                if (path[i][j] == 1) {
//                    System.out.printf("第 %d 个商品放入到背包\n", i);
//                }
//            }
//        }

        int i = path.length - 1; // 行的最大下标
        int j = path[0].length - 1; // 列的最大下标
        while (i > 0 && j > 0) { // 从 path 的最后开始找
            if (path[i][j] == 1) {
                System.out.printf("第 %d 个商品放入到背包\n", i);
                j -= w[i - 1];
            }
            i--;
        }
    }
}

KMP 算法

字符串匹配问题

字符串匹配问题:

  1. 有一个字符串 str1= "州州市杭州市你杭州杭州市你杭州市你杭州你好",和一个子串 str2="杭州市你杭州你"
  2. 现在要判断 str1 是否含有 str2,如果存在,就返回第一次出现的位置;如果没有,则返回-1

暴力匹配算法

如果用暴力匹配的思路,并假设现在 str1 匹配到 i 位置,子串str2匹配到 j 位置,则有:

  1. 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
  2. 如果失配(即str1[i]! = str2[j]),令 i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0
  3. 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间

暴力匹配算法代码实现如下:

java 复制代码
package com.algorithrm.kpm;

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 s1length = s1.length;
        int s2length = s2.length;
        int i = 0; // i 索引指向 s1
        int j = 0; // j 索引指向 s2

        while (i < s1length && j < s2length) { // 保证匹配时不越界
            if (s1[i] == s2[j]) {
                i++;
                j++;
            } else {
                // 如果失败,则让 i=i-(j-1);j=0
                i = i - (j - 1);
                j = 0;
            }
        }

        // 判断是否匹配成功
        if (j == s2length) {
            return i - j;
        } else {
            return -1;
        }
    }
}

KMP 算法介绍

  1. KMP是一个解决"模式串在文本串是否出现过,如果出现过,最早出现的位置"的经典算法
  2. Knuth-Morris-Pratt 字符串查找算法,简称为 "KMP算法",常用于在一个文本串S内查找一个模式串P的出现位置。这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法
  3. KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到前面匹配过的位置,省去了大量的计算时间
  4. 参考资料:www.cnblogs.com/ZuoAndFutur...

KMP 算法实践:字符串匹配问题

需求描述:

  1. 有一个字符串 str1= "BBC ABCDAB ABCDABCDABDE",和一个子串 str2="ABCDABD"
  2. 现在要判断 str1 是否含有 str2,如果存在,就返回第一次出现的位置,如果没有,则返回-1
  3. 要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法

思路分析:

下面介绍《部分匹配表》是怎么产生的:

算法代码实现

java 复制代码
package com.algorithrm.kpm;

import java.util.Arrays;

public class KMPAlgorithm {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
        int[] next = kmpNext(str2); // [0, 0, 0, 0, 1, 2, 0]
        System.out.println("next = " + Arrays.toString(next));
        int index = kmpSearch(str1, str2, next); // 15
        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++) {
            // 需要处理 str1.charAt(i) != str2.charAt(j),去调整j的大小
            while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
                j = next[j - 1]; // 核心点是调整j,在部分匹配表里
            }
            if (str1.charAt(i) == str2.charAt(j)) {
                j++;
            }
            if (j == str2.length()) {
                return i - j + 1;
            }
        }
        return -1;
    }

    /**
     * 获得一个字符串(子串)的部分匹配值表
     *
     * @param dest 字符串子串,如 "ABCDABD"
     * @return 匹配值表数组
     */
    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
            // 直到我们发现有 dest.charAt(i) == dest.charAt(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;
    }
}

贪心算法

算法介绍

贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。

贪婪算法所得到的结果不一定是最优的结果**(** 有时候会是最优解**)**,但是都是相对近似(接近)最优解的结果。

算法实践:集合覆盖

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

广播台 覆盖地区
K1 "北京", "上海", "天津"
K2 "广州", "北京", "深圳"
K3 "成都", "上海", "杭州"
K4 "上海", "天津"
K5 "杭州", "大连"

思路分析:

如何找出覆盖所有地区的广播台的集合呢?使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有 2ⁿ -1 个。假设每秒可以计算10个子集,如下表所示:

广播台数量n 子集总数2ⁿ 需要的时间
5 32 3.2秒
10 1024 102.4秒
32 4294967296 13.6年
100 1.26*100³º 4x10²³年

使用贪婪算法,效率高:

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

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

算法代码实现

java 复制代码
package com.algorithrm.greedy;

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<>();
        HashSet<String> hashSetK1 = new HashSet<>();
        hashSetK1.add("北京");
        hashSetK1.add("上海");
        hashSetK1.add("天津");
        HashSet<String> hashSetK2 = new HashSet<>();
        hashSetK2.add("广州");
        hashSetK2.add("北京");
        hashSetK2.add("深圳");
        HashSet<String> hashSetK3 = new HashSet<>();
        hashSetK3.add("成都");
        hashSetK3.add("上海");
        hashSetK3.add("杭州");
        HashSet<String> hashSetK4 = new HashSet<>();
        hashSetK4.add("上海");
        hashSetK4.add("天津");
        HashSet<String> hashSetK5 = new HashSet<>();
        hashSetK5.add("杭州");
        hashSetK5.add("大连");
        broadcasts.put("K1", hashSetK1);
        broadcasts.put("K2", hashSetK2);
        broadcasts.put("K3", hashSetK3);
        broadcasts.put("K4", hashSetK4);
        broadcasts.put("K5", hashSetK5);
        // 存放所有地区的集合
        HashSet<String> allAreas = new HashSet<>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");
        // 存放选择了的电台集合
        ArrayList<String> selects = new ArrayList<>();
        // 定义临时集合,在遍历过程中,存放遍历过程中的电台覆盖的地区和当前没有覆盖的地区的交集(地区K和allAreas的交集)
        HashSet<String> tempSet = new HashSet<>();
        // 定义 maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key
        // 如果 maxKey 不为 null,则会加入到 selects
        String maxKey = null;

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

        System.out.println("选择的结果是 = " + selects);
    }
}

贪心算法注意事项和细节

  1. 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
  2. 比如上题的算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区
  3. 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区。如果K2的使用成本低于K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的

普里姆算法

应用场景:修路问题

看一个应用场景和问题:

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

普通思路: 将10条边连接即可,但是总的里程数不是最小

正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少

最小生成树

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

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

算法介绍

普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图

普利姆的算法如下:

  1. G=(V,E)是连通网,T=(U,D)是最小生成树,V、U是顶点集合,E、D是边的集合
  2. 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
  3. 若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
  4. 重复步骤 2,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边

图解普利姆算法:

算法实践:修路问题

java 复制代码
package com.algorithrm.prim;

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

    public MGraph(int verxs) {
        this.verxs = verxs;
        data = new char[verxs];
        weight = new int[verxs][verxs];
    }
}
java 复制代码
package com.algorithrm.prim;

import java.util.Arrays;

/**
 * 最小生成树(就是需求里的村庄的图)
 */
public class MinTree {
    /**
     * 创建图的邻接矩阵
     *
     * @param graph  图对象
     * @param verxs  图对应的顶点个数
     * @param data   图的各个顶点的值
     * @param weight 图的邻接矩阵
     */
    public void createGraph(MGraph graph, int verxs, char[] data, int[][] weight) {
        for (int i = 0; i < verxs; i++) {
            graph.data[i] = data[i];
            for (int j = 0; j < verxs; j++) {
                graph.weight[i][j] = weight[i][j];
            }
        }
    }

    /**
     * 显示图的邻接矩阵
     *
     * @param graph
     */
    public void showGraph(MGraph graph) {
        for (int[] link : graph.weight) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     * prim算法,得到最小生成树
     *
     * @param graph 图
     * @param v     表示从图的第几个顶点开始,比如 A->0
     */
    public void prim(MGraph graph, int v) {
        int[] visited = new int[graph.verxs]; // 标记节点是否被访问过。默认是 0 表示都没有访问过
        visited[v] = 1; // 先把当前这个节点标记为已访问
        int h1 = -1, h2 = -1; // 记录两个顶点的下标
        int minWeight = 10000; // 将 minWeight 初始成一个大数,后面在遍历过程中,会被替换
        // 因为有 graph.verxs 个节点,普利姆算法结束后,有 graph.verxs-1 条边
        for (int k = 1; k < graph.verxs; k++) {
            // 这个是确定每一次生成的子图,和哪个节点的距离最近
            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 重新设置为最大值
        }
    }
}
java 复制代码
package com.algorithrm.prim;

public class PrimAlgorithm {
    public static void main(String[] args) {
        char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int verxs = data.length;
        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 graph = new MGraph(verxs);
        MinTree minTree = new MinTree();
        minTree.createGraph(graph, verxs, data, weight);

        minTree.showGraph(graph);
        minTree.prim(graph, 0);
    }
}

克鲁斯卡尔算法

算法介绍

克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法

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

具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。

应用场景:公交站问题

需求场景:某城市新增7个站点(A, B, C, D, E, F, G),现在需要修路把7个站点连通。各个站点的距离用边线表示(权),比如 A -- B 距离 12公里。

问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

在含有 n 个顶点的连通图中选择 n-1 条边,构成一颗极小连通子图,并使该连通子图中 n-1 条边上权值之和达最小,则称其为连通网的最小生成树。

算法图解

以城市公交站问题来图解,说明克鲁斯卡尔算法的原理和步骤:

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决以下两个问题:

  1. 对图的所有边按照权值大小进行排序
    1. 解决办法:采用排序算法进行排序。
  2. 将边添加到最小生成树中时,怎么判断是否形成了回路
    1. 解决办法:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

下图举例说明,如何判断是否构成回路:

关于终点的说明:

  1. 就是将所有顶点按照从小到大的顺序排列好之后:某个顶点的终点就是"与它连通的最大顶点"
  2. 因此,接下来,虽然<C,E>是权值最小的边,但是 C 和 E 的终点都是 F(即他们的终点相同)。因此,将<C,E>加入最小生成树的话就会形成回路。这就是判断回路的方式,也是就是说,我们加入的边的两个顶点不能都指向同一个终点,否则将构成回路。

算法实践:公交站问题

java 复制代码
package com.algorithrm.kruskal;

/**
 * 边的类
 */
public 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;
    }

    @Override
    public String toString() {
        return "EData{" +
                "start=" + start +
                ", end=" + end +
                ", weight=" + weight +
                '}';
    }
}
java 复制代码
package com.algorithrm.kruskal;

import java.util.Arrays;

public class KruskalCase {
    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 = new KruskalCase(vertexs, matrix);
        kruskalCase.print();
        kruskalCase.kruskal();
    }

    private int edgeNum; // 边的个数,12
    private char[] vertexs; // 顶点数组
    private int[][] matrix; // 邻接矩阵
    private static final int INF = Integer.MAX_VALUE; // 表示两个顶点不能连通

    // 构造器
    public KruskalCase(char[] vertexs, int[][] matrix) {
        int vlen = vertexs.length; // 7
        // 初始化顶点数和边的个数
        this.vertexs = new char[vlen];
        for (int i = 0; i < vlen; 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) {
                    edgeNum++; // 12
                }
            }
        }
    }

    /**
     * 获取下标为 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[] 数组中,后面需要遍历该数组。
     * 通过 matrix 邻接矩阵来获取。
     * EData[] 的形式:[['A','B',12],['B','F',7],...]
     *
     * @return
     */
    private EData[] getEdges() {
        int index = 0;
        EData[] edges = new EData[edgeNum]; // 12
        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;
    }

    /**
     * @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;
            }
        }
        return -1; // 找不到返回-1
    }

    /**
     * 对边进行排序处理,冒泡排序
     *
     * @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;
                }
            }
        }
    }

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

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

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

        // 按照边的权值大小进行排序(从小到大)
        sortEdges(edges);
        System.out.println("排序后图的边的集合 = \n" + Arrays.toString(edges) + "\n共" + edges.length + "条边");

        // 遍历 edges 数组,将边添加到最小生成树中时,判断是准备加入的边是否形成了回路。如果没有就加入rets,否则不能加入
        for (int i = 0; i < edgeNum; i++) {
            // 获取到第 i 条边的第一个顶点(起点)
            int p1 = getPosition(edges[i].start); // p1=4,E
            // 获取到第 i 条边的第二个顶点(终点)
            int p2 = getPosition(edges[i].end); // p2=5,F
            // 获取 p1 这个顶点在已有最小生成树中的终点
            int m = getEnd(ends, p1); // m=4,E
            // 获取 p2 这个顶点在已有最小生成树中的终点
            int n = getEnd(ends, p2); // n=5,F
            if (m != n) {
                // 设置 m 在已有最小生成树中的终点 <E,F>,相当于 [0,0,0,0,5,0,0,0,0,0,0,0]
                // 全部执行完毕后的 ends = [6,5,3,5,5,6,0,0,0,0,0,0]
                ends[m] = n;
                // 有一条边加入到 rets 数组
                rets[index++] = edges[i];
            }
        }

        // <E,F><C,D><D,E><B,F><E,G><A,B>
        // 统计并打印最小生成树,输出 rets
        System.out.println("最小生成树为:");
        for (int i = 0; i < index; i++) {
            System.out.println(rets[i]);
        }
    }
}

迪杰斯特拉算法

应用场景:最短路径

看一个应用场景和问题:

  1. 战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G),现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
  2. 各个村庄的距离用边线表示(权) ,比如 A -- B 距离 5公里
  3. 问:如何计算出G村庄到其它各个村庄的最短距离?
  4. 如果从其它点出发到各个点的最短距离又是多少?

算法介绍

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

算法图解

设置出发顶点为v,顶点集合 V{v1,v2,vi...},v到V中各顶点的距离构成距离集合 Dis{d1,d2,di...},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di):

  1. 从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
  2. 更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的)
  3. 重复执行上面两步骤,直到最短路径顶点为目标顶点即可结束

算法代码实现

java 复制代码
package com.algorithrm.dijkstra;

import java.util.Arrays;

/**
 * 图
 */
public 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));
        }
    }

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

    /**
     * 迪杰斯特拉算法实现
     *
     * @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(); // 选择并返回新的访问顶点,比如 G 的下一个点是 A
            update(index); // 更新 index 顶点到周围顶点的距离和前驱顶点
        }
    }
}
java 复制代码
package com.algorithrm.dijkstra;

import java.util.Arrays;

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

    /**
     * 构造器
     *
     * @param length 表示顶点的个数
     * @param index  出发顶点对应的下标,比如 G 顶点的下标是 6
     */
    public VisitedVertex(int length, int index) {
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];

        this.already_arr[index] = 1; // 设置出发顶点被访问过
        Arrays.fill(dis, 65535); // 访问距离全部设置为最大值
        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 getDisk(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.print("already_arr = ");
        for (int i : already_arr) {
            System.out.print(i + " ");
        }
        System.out.println();
        System.out.print("dis = ");
        for (int i : dis) {
            System.out.print(i + " ");
        }
        System.out.println();
        System.out.print("pre_visited = ");
        for (int i : pre_visited) {
            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.print("N ");
            }
            count++;
        }
        System.out.println();
    }
}
java 复制代码
package com.algorithrm.dijkstra;

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 = new Graph(vertex, matrix);
        graph.showGraph(); // 测试显示图的邻接矩阵

        graph.dsj(6); // G
        // already_arr = 1 1 1 1 1 1 1
        // dis = 2 3 9 10 4 6 0
        // pre_visited = 6 6 0 5 6 6 0
        // A(2)B(3)C(9)D(10)E(4)F(6)G(0)
        graph.showDijkstra();
    }
}

弗洛伊德算法

算法介绍

和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。

弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径 ,迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径

弗洛伊德算法 VS 迪杰斯特拉算法:

  1. 迪杰斯特拉算法通过选定的被访问顶点求出从出发访问顶点到其他顶点的最短路径
  2. 弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

算法图解

  1. 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为Lij,则 vi 到 vj 的最短路径为: min((Lik+Lkj),Lij) ,vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径
  2. 至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得

弗洛伊德算法图解分析如下:

算法实践:最短路径

java 复制代码
package com.algorithrm.floyd;

import java.util.Arrays;

public class Graph {
    private char[] vertex; // 存放顶点数组
    private int[][] dis; // 保存从各个顶点出发到其他顶点的距离
    private int[][] pre; // 保存到达目标顶点的前驱顶点

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

    /**
     * 弗洛伊德算法
     */
    public void floyd() {
        int len = 0; // 保存距离
        // 对中间顶点遍历,k就是中间顶点的下标 [A,B,C,D,E,F,G]
        for (int k = 0; k < dis.length; k++) {
            //从 i 顶点开始出发 [A,B,C,D,E,F,G]
            for (int i = 0; i < dis.length; i++) {
                // 到达 j 顶点 [A,B,C,D,E,F,G]
                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]; // 更新前驱顶点
                    }
                }
            }
        }
    }

    /**
     * 显示pre数组和dis数组
     */
    public void show() {
        // char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        for (int k = 0; k < dis.length; k++) {
            for (int i = 0; i < dis.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("\n");
        }
    }
}
java 复制代码
package com.algorithrm.floyd;

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(matrix, vertex);
        graph.floyd();
        graph.show();
    }
}

马踏棋盘算法

算法介绍

马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。

算法图解

  1. 马踏棋盘问题(骑士周游问题)实际上是**图的深度优先搜索(DFS)**的应用
  2. 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,如图:走到了第53个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯......
  3. 分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化

算法代码实现

java 复制代码
package com.algorithrm.horse;

import java.awt.*;
import java.util.ArrayList;
import java.util.Comparator;

public class HorseChessboard {
    private static int X; // 棋盘的列数(X轴)
    private static int Y; // 棋盘的行数(Y轴)
    private static boolean[] visited; // 标记棋盘的各个位置是否被访问过
    private static boolean finished; // 标记是否棋盘所有位置都被访问过,如果为true表示成功

    /**
     * 骑士周游问题算法代码
     *
     * @param chessboard 棋盘二维数组
     * @param row        马当前的位置行,从 0 开始,就是 Y 轴
     * @param column     马当前的位置列,从 0 开始,就是 X 轴
     * @param step       是第几步,初始位置就是第 1 步
     */
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
        chessboard[row][column] = step;
        // 标记位置已经访问过。当前行*列数 + 当前列,就是在一维数组中的位置下标
        // row = 4, X = 8,column = 4, 结果为 36
        visited[row * X + column] = true;
        // 获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column, row));
        // 对 ps 进行排序,规则就是对 ps 的所有 Point 对象的下一步的位置的数目、进行非递减排序
        sort(ps);
        // 遍历 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对象),并放入到一个集合中,最多有 8 个位置
     *
     * @param curPoint 当前位置
     * @return 可走位置集合
     */
    public static ArrayList<Point> next(Point curPoint) {
        ArrayList<Point> ps = new ArrayList<>();
        Point p1 = new Point(); // 当前p1的x和y还不确定

        // 表示马儿可以走 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;
    }

    /**
     * 根据当前这个一步的所有下一步的选择位置,进行非递减排序,减少回溯次数。(先走下一步可选比较少的)
     *
     * @param ps 可以走的下一个位置的集合
     */
    public static void sort(ArrayList<Point> ps) {
        ps.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                // 获取到 o1 的下一步的所有位置个数
                int count1 = next(o1).size();
                // 获取到 o2 的下一步的所有位置个数
                int count2 = next(o2).size();
                if (count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }
            }
        });
    }

    public static void main(String[] args) {
        System.out.println("骑士周游算法,开始运行");
        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();
        }
    }
}

执行结果如下:

没执行 sort() 方法之前,耗时22244毫秒;添加 sort() 方法之后,耗时52毫秒

相关推荐
weiabc1 小时前
printf(“%lf“, ys) 和 cout << ys 输出的浮点数格式存在细微差异
数据结构·c++·算法
铸人1 小时前
大数分解的Shor算法-C#
开发语言·算法·c#
问好眼1 小时前
《算法竞赛进阶指南》0x01 位运算-3.64位整数乘法
c++·算法·位运算·信息学奥赛
yyjtx1 小时前
DHU上机打卡D31
开发语言·c++·算法
GEO行业研究员1 小时前
《认知锚定与路径锁死:基于爱搜光年模型的AI决策链条风险放大机制监测》
人工智能·算法·ai搜索优化·geo优化·医疗geo·医疗geo优化
wefg12 小时前
【算法】单调栈和单调队列
数据结构·算法
岛雨QA2 小时前
图「Java数据结构与算法学习笔记12」
数据结构·算法
czxyvX2 小时前
020-C++之unordered容器
数据结构·c++
岛雨QA2 小时前
多路查找树「Java数据结构与算法学习笔记11」
数据结构·算法