文章目录

  • 参考文献
  • 1. 分治算法
    • 1.1 基本介绍
    • 1.2 基本算法步骤
    • 1.3 算法实践——汉诺塔
  • 2. 动态规划算法
    • 2.1 核心思想
    • 2.2 算法实践——背包问题
  • 3. KMP算法
    • 3.1 应用场景-字符串匹配问题
    • 3.2 KMP算法介绍
      • 3.2.1 思路分析
      • 3.2.2 部分匹配表的产生
  • 4. 贪心算法
    • 4.1 算法介绍
    • 4.2 算法应用——集合覆盖
      • 4.2.1 思路分析
    • 4.3 算法应用——钱币找零
  • 5. 普利姆算法
    • 5.1 问题提出——修路问题
    • 5.2 最小生成树
    • 5.3 普利姆算法介绍
      • 5.3.1 普利姆算法步骤
      • 5.3.2 算法实践——修路问题
  • 6. 克鲁斯卡尔算法
    • 6.1 问题提出——公交站问题
    • 6.2 算法介绍
      • 6.2.1 算法图解说明
      • 6.2.2 算法分析
      • 6.2.3 代码实现
  • 7. 迪杰斯特拉算法
    • 7.1 问题提出——最短路径问题
    • 7.2 算法介绍
      • 7.2.1 算法过程
      • 7.2.2 算法实现
  • 8. 弗洛伊德算法
    • 8.1 算法介绍
    • 8.2 算法分析
    • 8.3 算法实践——最短路径问题
  • 9. 马踏棋盘游戏
    • 9.1 游戏介绍
    • 9.2 思路分析

参考文献

  1. 数据结构与算法
  2. https://www.bilibili.com/video/BV1E4411H73v?p=153
  3. https://cloud.tencent.com/developer/article/1805386
  4. 图解迪杰斯特拉算法(最短路径问题)

1. 分治算法

1.1 基本介绍

  • 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
  • 分治算法可以求解的一些经典问题
    • 二分搜索
    • 大整数乘法
    • 棋盘覆盖
    • 合并排序
    • 快速排序
    • 线性时间选择
    • 最接近点对问题
    • 循环赛日程表
    • 汉诺塔

1.2 基本算法步骤

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

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

1.3 算法实践——汉诺塔

  • A、B、C三个塔

    • 如果只有一个盘,直接A->C
    • 如果大于等于两个盘,就分成两部分。1. 最下面的一个盘为一部分;2. 上面的所有盘为一部分
      • 将上面的所有盘:A->B
      • 最下面的一个盘:A->C
      • 再将B中的盘:B->C
  • 代码实现

    package pers.chh3213.divide_and_conquer.hanoi;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.divide_and_conquer.hanoi* @ClassName : Hanoi.java* @createTime : 2022/2/19 11:28* @Email :* @Description :*/
    public class Hanoi {public static void main(String[] args) {hanoiTower(2,'A','B','C');}/*** 汉诺塔* @param num 盘子总数* @param a 塔A* @param b 塔B* @param c 塔C*/public static void hanoiTower(int num,char a,char b, char c){if(num==1) {System.out.printf("第%d个盘子从%s到%s",num,a,c);System.out.println();}else{//如果我们有n>=2情况,我们总是可以看做是两个盘1.最下边的一个盘2.上面的所有盘,//1.先把最上面的所有盘A->B, 移动过程会使用到chanoiTower(num-1,a,c,b);//2.把最下面的盘从A移动到CSystem.out.printf("第%d个盘子从%s到%s",num,a,c);System.out.println();//3.再将B中的盘:B->ChanoiTower(num-1,b,a,c);}}
    }
  • 递归更多讲解请参见这篇博客好文

2. 动态规划算法

2.1 核心思想

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

2.2 算法实践——背包问题

  • 有一个背包,容量为4磅,现有如下物品

    1. 要求达到的目标为装入的背包的总价值最大,并且重量不超出
    2. 要求装入的物品不能重复
  • 思路分析

  1. 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)

  2. 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

  3. 算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i]j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:

    //表示填入表的第一行和第一列是 0,主要是为了方便表示物品和容量
    (1) v[i][0]=v[0][j]=0; // 当准备加入新增的商品的重量大于当前背包的容量时,就直接使用上一个单元格的装入策略(装入物品的价值)
    (2) 当 w[i]>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][j-w[i]]}v[i-1][j]:上一个装法的总价值v[i] : 表示当前商品的价值v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的总价值
    
  • 简单来说:

    1. 装入物品的容量大于背包容量时,直接使用之前装入背包物品的最大价值
    2. 装入物品容量小于等于背包容量时,比较:装入该物品之前,背包物品的最大价值与装入该物品后,该物品的价值+剩余容量能放入物品的最大价值。而后选取其中较大者。
  • 代码实现

    package pers.chh3213.dynamic_program;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.dynamic_program* @ClassName : KnapsackProblem.java* @createTime : 2022/2/19 14:47* @Email :* @Description :*/
    public class KnapsackProblem {//存储最大价值private int[][] value;//用于表示物品放入背包的方式private int[][] method;//背包容量private int m;//物品个数private int n;public static void main(String[] args) {//各个物品的重量int[] w = {1,4,3};//物品价值int[] v = {1500,3000,2000};//背包容量int m = 4;//物品个数int n = v.length;KnapsackProblem knapsackProblem = new KnapsackProblem(m, n);knapsackProblem.DP(w,v);}public KnapsackProblem(int m, int n) {this.m = m;this.n = n;value = new int[n+1][m+1];method = new int[n+1][m+1];}/*** 动态规划* @param w 物品重量数组* @param v 物品价值数组*/public  void DP(int[] w, int[]v){///初始化第一行和第一列,这里在本程序中,可以不去处理,因为默认就是0for (int i = 0; i < value.length; i++) {value[i][0]=0;}for (int i = 0; i < value[0].length;i++) {value[0][i]=0;}//不处理第一行,i是从1开始的for (int i = 1; i < value.length; i++) {//不处理第一列,j是从1开始的for(int j = 1; j < value[0].length; j++){//因为我们程序i是从1开始的,因此原来公式中的w[i]修改成w[i-1]if(w[i-1]>j){value[i][j]= value[i-1][j];}else{//背包剩余的容量int remain = j-w[i-1];//如果放入该物品前的最大价值大于放入该物品后的最大价值,就不放入该物品if(value[i-1][j]>v[i-1]+value[i-1][remain]){value[i][j]=value[i-1][j];}else{value[i][j]=v[i-1]+value[i-1][remain];//存入放入方法method[i][j]=1;}}}}//打印放入背包的最大价值for (int[] val:value) {System.out.println(Arrays.toString(val));}//打印价值最大的放法//存放方法的二维数组的最大下标,从最后开始搜索存放方法int i = method.length - 1;int j = method[0].length - 1;while(i > 0 && j > 0) {if(method[i][j] == 1) {System.out.println("将第" + i + "个物品放入背包");//背包剩余容量j -= w[i-1];}i--;}}}

3. KMP算法

3.1 应用场景-字符串匹配问题

  • 字符串匹配问题:

有一个字符串 str1= BBC ABCDAB ABCDABCDABDE,和一个子串 str2=ABCDABD。现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

  • 暴力匹配

    package pers.chh3213.KMP;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.KMP* @ClassName : bruteForce.java* @createTime : 2022/2/19 15:31* @Email :* @Description :暴力解法解决字符串匹配问题*/
    public class bruteForce {public static void main(String[] args) {String str1="BBC ABCDAB ABCDABCDABDE";String str2="ABCDABD";int included = isIncluded(str1, str2);if(included!=-1){System.out.println("匹配成功");System.out.println("输出位置为"+included);}else{System.out.println("匹配失败");}}public static int isIncluded(String str1,String str2){char[] charArray1 = str1.toCharArray();char[] charArray2 = str2.toCharArray();for (int i = 0; i < charArray1.length; i++) {int index = i;int j;for (j = 0; j < charArray2.length&&index<charArray1.length;) {if(charArray1[index]==charArray2[j]){index++;j++;}else{break;}}if(j==charArray2.length){return i;}}return -1;}
    }
  • 用暴力方法解决的话就会有大量的回潮,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量,的时间。

3.2 KMP算法介绍

  • KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法,效率很高

  • KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间

  • 延续章节3.1的问题,要求使用KMP算法完成。

3.2.1 思路分析

  1. 首先,用 str1的第一个字符和 str2的第一个字符去比较,不符合,关键词向后移动一位

  2. 重复第一步,还是不符合,再后移

  3. 一直重复,直到 Str1有一个字符与 Str2的第一个字符符合为止

  4. 接着比较字符串和搜索词的下一个字符,还是符合

  5. 遇到 Str1有一个字符与 Str2对应的字符不符合

  6. 这时候,想到的是继续遍历 str1的下一个字符,重复第 1步。(其实是很不明智的,因为此时 BCD已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是” ABCDAB”。KMP 算法的想法是:设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。

  7. 怎么做到把刚刚重复的步骤省略掉?可以对 str2计算出一张部分匹配表,这张表的产生在后面介绍。
    str2的部分匹配表如下

  8. 已知空格与 D不匹配时,前面六个字符” ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的部分匹配值为 2,因此按照下面的公式算出向后移动的位数:
    移动位数 = 已匹配的字符数 - 对应的部分匹配值。因为 6 - 2 等于 4,所以将搜索词向后移动 4 位

  9. 因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(”AB”),对应的部分匹配值为0。所以,移动位数=2-0,结果为2,于是将搜索词向后移2位。

  10. 因为空格与A不匹配,继续后移一位。

  11. 逐位比较,直到发现 C与 D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位

  12. 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动 7 位,这里就不再重复了

3.2.2 部分匹配表的产生

  • 前缀与后缀

  • 部分匹配值就是”前缀”和”后缀”的最长的共有元素的长度。
    以”ABCDABD”为例:

    • ”A”的前缀和后缀都为空集,共有元素的长度为 0;
    • ”AB”的前缀为[A],后缀为[B],共有元素的长度为 0;
    • ”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度 0;
    • ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为 0;
    • ”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为 1;
    • ”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为 2;
    • ”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD,D],共有元素的长度为 0。
  • ”部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(” AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。

  • 代码实现

    package pers.chh3213.KMP;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.KMP* @ClassName : KMPSearch.java* @createTime : 2022/2/19 16:37* @Email :* @Description :*/
    public class KMPSearch {public static void main(String[] args) {String str1="BBC ABCDAB ABCDABCDABDE";String str2="ABCDABD";int[] table = getTable(str2);System.out.println("部分匹配表:"+Arrays.toString(table));//[0, 0, 0, 0, 1, 2, 0]int included = kmp(str1, str2,table);if(included!=-1){System.out.println("匹配成功");System.out.println("输出位置为"+included);//15}else{System.out.println("匹配失败");}}/*** 得到字符串的部分匹配表* @param matchStr 用于匹配的字符串* @return 部分匹配表*/public static int[] getTable(String matchStr){//部分匹配值的数组int[] partTable = new int[matchStr.length()];//字符串的第一个元素没有前缀与后缀,部分匹配值为0partTable[0]=0;//i用来指向部分匹配字符串末尾的字符,j用来指向开始的字符for (int i = 1,j=0; i < matchStr.length(); i++) {//当j>0且前缀后缀不匹配时while (j>0&&matchStr.charAt(i)!=matchStr.charAt(j)){//使用部分匹配表中前一个表项的值j = partTable[j-1];}//如果前缀后缀匹配,j向后移,继续比较if(matchStr.charAt(i)==matchStr.charAt(j)){j++;}//存入匹配值partTable[i]=j;}return partTable;}/*** kmp搜索算法* @param str1 原字符串* @param str2 字串* @param partTable 字串对应的部分匹配表* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置,*/public static int kmp(String str1,String str2,int[] partTable){for (int i = 0,j=0; i < str1.length(); i++) {while (j>0&&str1.charAt(i)!=str2.charAt(j)){j=partTable[j-1];}if(str1.charAt(i)==str2.charAt(j)){j++;}if(j==str2.length()){//如果匹配完成,返回第一个字符出现位置return i-j+1;}}return -1;}
    }

4. 贪心算法

4.1 算法介绍

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

4.2 算法应用——集合覆盖

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

4.2.1 思路分析

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

图解

  • 遍历电台的覆盖地区,发现K1覆盖的地区最多,将K1覆盖的地区从地区集合中移除。然后将K1放入电台集合中,并更新覆盖地区个数

  • 遍历,发现K2覆盖的地区最多,将K2覆盖的地区从地区集合中移除。然后将K2放入电台集合中,并更新覆盖地区个数

  • 遍历,发现K3覆盖的地区最多,将K3覆盖的地区从地区集合中移除。然后将K3放入电台集合中,并更新覆盖地区个数

  • 遍历,发现K5覆盖的地区最多,将K5覆盖的地区从地区集合中移除。然后将K5放入电台集合中,并更新覆盖地区个数。所有区域都被覆盖,算法结束

  • 关键代码示例

    /**** @param allAreas 存放的所有地区* @param broadcasts  所有广播电台* @return*/public ArrayList<String> greedyMethod(HashSet<String>allAreas, HashMap<String,HashSet<String>>broadcasts){//创建ArrayList,存放选择的电台集合ArrayList<String> selects = new ArrayList<>();//定义一个临时的集合,在遍历的过程中,//存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的HashSet<String> tempSet = new HashSet<>();//定义给maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key//如果maxKey不为null,则会加入到selectsString maxKey=null;//如果allAreas不为0,则表示还没有覆盖到所有的地区while (allAreas.size()!=0){maxKey=null;//遍历broadcasts,取出对应 keyfor (String key:broadcasts.keySet()) {//每进行一次fortempSet.clear();//当前这个key能够覆盖的地区HashSet<String> areas = broadcasts.get(key);tempSet.addAll(areas);//求出tempSet和allAreas集合的交集,交集会赋给tempSettempSet.retainAll(allAreas);//如果当前这个集合包含的未覆盖地区的数量,比maxKey指向的集合地区还多//就需要重置maxKeyif(tempSet.size()>0&&(maxKey==null||tempSet.size()>broadcasts.get(maxKey).size())){maxKey = key;}}//maxKey != null, 应该将maxKey加入 selectsif(maxKey!=null){selects.add(maxKey);///将maxKey指向的广播电台覆盖的地区,从allAreas去掉allAreas.removeAll(broadcasts.get(maxKey));}}return selects;}
    
  • 完整代码见于gitee仓库

4.3 算法应用——钱币找零

  • 假设纸币金额为1元、5元、10元、20元、50元、100元,用尽可能少的纸币数量凑成123元。

  • 尽可能从大面值一直往下减即可

  • 代码实现

    package pers.chh3213.greedy;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.greedy* @ClassName : MoneyChange.java* @createTime : 2022/2/19 20:04* @Email :* @Description :*/
    public class MoneyChange {public static void main(String[] args) {int[] cases = {100,50, 20, 10, 5, 1};//需要从大到小排列int[] arrCases = change(123,cases);System.out.println(Arrays.toString(arrCases));//打印结果for(int i = 0; i<cases.length; i++) {if(arrCases[i] != 0) {System.out.println("需要" + cases[i] + "元的纸币" + arrCases[i] + "张");}}}/*** 凑钱* @param money 总金额* @param cases 纸币数组* @return 返回每张钱币要拿出的数量组成的数组*/public static int[] change(int money,int[]cases){int leftMoney = money;int[] counter = new int[cases.length];if(money<0){System.out.println("输入金额为负数!!");return null;}while (leftMoney>0){for (int i = 0; i < cases.length; i++) {int count=0;while(leftMoney-cases[i]>=0){count++;leftMoney = leftMoney-cases[i];}counter[i]=count;}}return counter;}
    }

5. 普利姆算法

5.1 问题提出——修路问题

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

5.2 最小生成树

  • 修路问题本质就是就是最小生成树问题
  • 最小生成树(Minimum Cost Spanning Tree),简称 MST。给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。
    1. N个顶点,一定有N-1条边
    2. 包含全部顶点
    3. N-1条边都在图中
    4. 求最小生成树的算法主要是普里姆算法克鲁斯卡尔算法

5.3 普利姆算法介绍

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

5.3.1 普利姆算法步骤

  • 设G=(V,E)G=(V, E)G=(V,E)是具有n个顶点的连通网, T=(U,TE)T=(U, TE)T=(U,TE)是G的最小生成树, T的初始状态为U=u0(u0∈V)U={u0} (u0\in V)U=u0(u0∈V), TE={}。重复执行下述操作:在所有u∈Uu\in Uu∈U, v∈V−Uv\in V-Uv∈V−U的边中找一条代价最小的边(u,v)(u,v)(u,v)并入集合TE,同时v并入U,直至U=V。
  • Prim算法的基本思想用伪代码描述如下;

5.3.2 算法实践——修路问题

  • MGraph类代码

    package pers.chh3213.prim;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : MGraph.java* @createTime : 2022/2/20 9:55* @Email :* @Description :图类的创建*/
    public class MGraph {int vertex;char[] data;int[][] weight;public MGraph(int vertex) {//表示图的顶点个数this.vertex = vertex;//存放顶点数据data = new char[vertex];//存放边,就是我们的邻接矩阵weight = new int[vertex][vertex];}
    }
  • MinTree类代码

    package pers.chh3213.prim;import java.util.Arrays;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : MinTree.java* @createTime : 2022/2/20 9:58* @Email :* @Description :根据村庄的图创建最小生成树*/
    public class MinTree {//图对象MGraph graph;//图的各个顶点的值char[] data;//图对应的顶点int vertex;//图的邻接矩阵int[][]weight;public MinTree(char[] data, int[][] weight) {this.data = data;this.weight = weight;this.vertex = data.length;this.graph = new MGraph(this.vertex);}/*** 创建图的邻接矩阵*/public void createGraph(){int i,j;for (i = 0; i < vertex; i++) {graph.data[i]=data[i];for (j=0;j<vertex;j++){graph.weight[i][j]=weight[i][j];}}}/*** 显示图的邻接矩阵*/public void showGraph(){for (int[] link: graph.weight) {System.out.println(Arrays.toString(link));}}/*** 编写prim算法,得到最小生成树* @param v 表示从图的第几个顶点开始生成*/public void prim(int v){///visited[]标记结点(顶点)是否被访问过int[] visited = new int[graph.vertex];//把当前这个结点标记为已访问visited[v]=1;//h1和h2记录两个顶点的下标int h1 = -1;int h2 = -1;//将minWeight初始成一个大数,后面在遍历过程中,会被替换,int minWeight = 10000;//因为有graph.verxs顶点,普利姆算法结束后,有graph.verxs-1边for (int i = 1; i < graph.vertex; i++) {//确定每一次生成的子图,和哪个结点的距离最近for (int j = 0; j < graph.vertex ; j++) {//j结点表示被访问过的结点for (int k = 0; k < graph.vertex; k++) {//k结点表示未被访问过的结点if(visited[j]==1&&visited[k]==0&&graph.weight[j][k]<minWeight){minWeight = graph.weight[j][k];h1=j;h2=k;}}}//找到一条边是最小System.out.println("边<" + graph.data[h1] +"," + graph.data[h2] + ">权值 :" + minWeight);//将当前这个结点标记为已经访问visited[h2]=1;//minweight 重新设置为最大值10000minWeight=10000;}}}
  • PrimDemo类代码

    package pers.chh3213.prim;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.prim* @ClassName : PrimDemo.java* @createTime : 2022/2/20 9:54* @Email :* @Description :*/
    public class PrimDemo {public static void main(String[] args) {char[] data = new char[]{'A','B','C','D','E','F','G'};//邻接矩阵的关系使用二维数组表示,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}};MinTree minTree = new MinTree(data, weight);minTree.createGraph();minTree.showGraph();//普利姆算法生成最小生成树minTree.prim(0);}
    }
  • PrimDemo代码运行结果为:

    /*
    边<A,G>权值 :2
    边<G,B>权值 :3
    边<G,E>权值 :4
    边<E,F>权值 :5
    边<F,D>权值 :4
    边<A,C>权值 :7*/
    
  • 代码仓库见于gitee

6. 克鲁斯卡尔算法

6.1 问题提出——公交站问题

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

6.2 算法介绍

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

6.2.1 算法图解说明

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

  • 对于如上图G4所示的连通网可以有多棵权值总和不相同的生成树。

  • 假设用数组R保存最小生成树结果

  • 第1步:将边<E,F>加入R中。边<E,F>的权值最小,因此将它加入到最小生成树结果R中。

  • 第2步:将边<C,D>加入R中。上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。

  • 第3步:将边<D,E>加入R中。上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。

  • 第4步:将边<B,F>加入R中。上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路:因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中。

  • 第5步:将边<E,G>加入R中。
    上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。

  • 第6步:将边<A,B>加入R中。上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路:因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中。

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

6.2.2 算法分析

克鲁斯卡尔算法重点需要解决的以下两个问题:

  • 问题一:对图的所有边按照权值大小进行排序。采用排序算法进行排序即可。

  • 问题二:将边添加到最小生成树中时,怎么样判断是否形成了回路。处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。
    然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

  • 举例

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

6.2.3 代码实现

参考gitee仓库

7. 迪杰斯特拉算法

7.1 问题提出——最短路径问题

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

7.2 算法介绍

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

7.2.1 算法过程

  1. 设置出发顶点为v,顶点集合V(vl,v2,vi…), v到V中各顶点的距离构成距离集合Dis, Dis (d1,d2,di…), Dis集合记录着v到图中各顶点的距离(到自身可以看作0, v到vi距离对应为di)
  2. 从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
  3. 更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的)
  4. 重复执行两步骤,直到最短路径顶点为目标顶点即可结束

7.2.2 算法实现

代码实现来源于 参考资料4.

package pers.chh3213.dijkstra;import java.util.ArrayList;public class Dijkstra {// 约定 10000 代表距离无穷大public static void main(String[] args) {// 顶点char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };int[][] weight = {   // 图的邻接矩阵/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/{0,   5,   7,   INF,   INF,   INF,   2},/*B*/{5,   0,   INF,   9,   INF,   INF,   3},/*C*/{7,   INF,   0,   INF,   8,   INF,   INF},/*D*/{INF,   9,   INF,   0,   INF,   4,   INF},/*E*/{INF,   INF,   8,   INF,   0,   5,   4},/*F*/{INF,   INF,   INF,   4,   5,   0,   6},/*G*/{2,   3,   INF,   INF,   4,   6,   0}};// 起点下标int source = 6;// 使用迪杰斯特拉查找最短路径int[] dis = dijkstra(source, vertexes, weight);// 输出最短路径长度for (int i=0; i<dis.length; i++){System.out.println(vertexes[source] + "->" + vertexes[i] + " = " + dis[i]);}}private final static int INF = 10000;/*** 迪杰斯特拉算法求解最短路径问题* @param source    起点下标* @param vertexes  顶点集合* @param weight    邻接矩阵* @return int[]    起点到各顶点最短路径距离*/public static int[] dijkstra(int source, char[] vertexes, int[][] weight){// 记录起点到各顶点的最短路径长度,如 dis[2] 表示起点到下标为 2 的顶点的最短路径长度int[] dis;// 存储已经求出到起点最短路径的顶点。ArrayList<Character> S = new ArrayList<>();/* 初始化起点 */dis = weight[source];S.add(vertexes[source]);/* 当 S 集合元素个数等于顶点个数时,说明最短路径查找完毕 */while(S.size() != vertexes.length){int min = INF;// 记录已经求出最短路径的顶点下标int index = -1; /* 从 V-S 的集合中找距离起点最近的顶点 */for (int j=0; j<weight.length; j++){if (!S.contains(vertexes[j]) && dis[j] < min){min = weight[source][j];index = j;}}// 更新起点到该顶点的最短路径长度dis[index] = min;// 将顶点加入到 S 集合中,即表明该顶点已经求出到起点的最小路径S.add(vertexes[index]);/* 更新起点经过下标为 index 的顶点到其它各顶点的最短路径 */for (int m=0; m<weight.length; m++){if (!S.contains(vertexes[m]) && dis[index] + weight[index][m] < dis[m]){dis[m] = dis[index] + weight[index][m];}}}return dis;}
}

8. 弗洛伊德算法

8.1 算法介绍

  1. 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特,弗洛伊德命名
  2. 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
  3. 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
  4. 弗洛伊德算法VS迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

8.2 算法分析

  1. 假设顶点viv_ivi​到顶点vkv_kvk​的最短路径己知为LikL_{ik}Lik​,顶点vkv_kvk​到vjv_jvj​的最短路径己知为LkjL_{kj}Lkj​,顶点viv_ivi​到vjv_jvj​的路径为LijL_{ij}Lij​,则viv_ivi​到vjv_jvj​的最短路径为: min((Lik+Lkj),Lij)min((L_{ik}+L_{kj}),L_{ij})min((Lik​+Lkj​),Lij​), vkv_kvk​的取值为图中所有顶点,则可获得viv_ivi​到vjv_jvj​的最短路径
  2. 至于viv_ivi​到vkv_kvk​的最短路径LikL_{ik}Lik​或者vkv_kvk​到vjv_jvj​的最短路径LkjL_{kj}Lkj​,是以同样的方式获得。

8.3 算法实践——最短路径问题

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

  • 代码实现

    package pers.chh3213.floyd;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.floyd* @ClassName : FloydAlgo.java* @createTime : 2022/2/20 15:14* @Email :* @Description :*/
    public class FloydAlgo {public static void main(String[] args) {int INF = 10000;// 顶点char[] vertexes = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };int[][] dis = {   // 图的邻接矩阵/*A*//*B*//*C*//*D*//*E*//*F*//*G*//*A*/{0,   5,   7,   INF,   INF,   INF,   2},/*B*/{5,   0,   INF,   9,   INF,   INF,   3},/*C*/{7,   INF,   0,   INF,   8,   INF,   INF},/*D*/{INF,   9,   INF,   0,   INF,   4,   INF},/*E*/{INF,   INF,   8,   INF,   0,   5,   4},/*F*/{INF,   INF,   INF,   4,   5,   0,   6},/*G*/{2,   3,   INF,   INF,   4,   6,   0}};FloydAlgo floydAlgo = new FloydAlgo();int[][] newDis = floydAlgo.floyd(dis);floydAlgo.show(vertexes,newDis);}/*** 弗洛伊德算法* @param dis  从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组*/public int[][] floyd(int[][] dis){//遍历中转顶点for (int k = 0; k < dis.length; k++) {//从i顶点出发for (int i = 0; i < dis.length; i++) {//到达j顶点for (int j = 0; j < dis.length; j++) {if(dis[i][k]+dis[k][j]<dis[i][j]){dis[i][j]=dis[i][k]+dis[k][j];}}}}return dis;}/*** 输出每一对顶点之间的最短距离* @param vertex 顶点数组* @param dis 距离数组*/public void show(char[] vertex,int[][] dis){for (int i = 0; i < dis.length; i++) {for (int j = 0; j < dis[i].length; j++) {System.out.print(vertex[i]+"->"+vertex[j]+"="+dis[i][j]+"\t");}System.out.println();}}
    }
  • 运行结果为

9. 马踏棋盘游戏

9.1 游戏介绍

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

9.2 思路分析

马踏棋盘问题实际上是图的深度优先搜索(DFS)的应用。

  • 假设以 V 为起点,首先找出在指定规则下 V 点下一步可能的落点。
  • 在下一步的可能的落点中选择一个点(假设是 U 点),然后走到 U 点。
  • 再以 U 点为起点,找出指定规则下 U 点下一步可能的落点。
  • 在下一步可能的落点中选择一个点(假设是 W 点),然后走到 W 点。
  • 如此循环下去,直至走完了所有的格子。

需要注意的是,在下一步可能的落点中选择一个点这个操作具体怎么进行呢?是随机选择一个点的吗?

并不是随机的,这里面用到了贪心算法:为了让之后的选择尽可能少一点,一般会在下一步可能的落点选项中优先选择这样的一个点,这个点的特点是它的下一步可能的落点的选择最少。

  • 代码实现(代码中的Point类为java库,在包package java.awt下)

    package pers.chh3213.horse_chessboard;import java.awt.*;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Comparator;/*** Created with IntelliJ IDEA.** @author : chh3213* @version : 1.0* @Project : DataStructures_Algorithms* @Package : pers.chh3213.horse_chessboard* @ClassName : HorseChessboard.java* @createTime : 2022/2/20 15:48* @Email :* @Description :*/
    public class HorseChessboard {//棋盘的行数private static int X=8;//棋盘的列数private static int Y=8;//创建一个数组,标记棋盘的各个位置是否被访问过private static boolean[][] isVisited;//1使用一个属性,标记是否棋盘的所有位置都被访问// 如果为 true,表示成功private static boolean isFinished;public static void main(String[] args) {//马儿初始位置的行,从1开始编号,int row = 1;//马儿初始位置的列,从1开始编号,int column=1;//创建棋盘int[][] chessboard = new int[X][Y];//初始值都是falseisVisited = 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[] item:chessboard) {System.out.println(Arrays.toString(item));}}/*** 完成骑士周游问题的算法* @param chessboard 棋盘* @param row 当前位置的行,从0开始* @param col 当前位置的列,从0开始* @param step 第几步,初始位置是第一步*/public static void traversalChessboard(int[][] chessboard,int row,int col, int step){chessboard[row][col]=step;isVisited[row][col]=true;ArrayList<Point> nextPoints = next(new Point(row, col));sort(nextPoints);while (!nextPoints.isEmpty()){// 排序后,移动到的下一个点的下一步的可选项个数时最小的Point point = nextPoints.remove(0);//若没有访问过//System.out.println(point.x);//System.out.println(point.y);if(!isVisited[point.x][point.y]){traversalChessboard(chessboard,point.x, point.y, step+1);}}//判断马儿是否完成了任务,使用 step和应该走的步数比较,//如果没有达到数量,则表示没有完成任务,将整个棋盘置0)//说明: step<X*Y 成立的情况有两种//  1.棋盘到目前位置,仍然没有走完//  2.棋盘处于一个回溯过程if(step<X*Y&&!isFinished){chessboard[row][col]=0;isVisited[row][col]=false;}else{isFinished=true;}}/*** *功能:根据当前位置(Point对象),计算马儿还能走哪些位置(Point),* 并放入到一个集合中(ArrayList),最多有8个位置* @param curPoint 当前位置* @return*/public static ArrayList<Point>next(Point curPoint){ArrayList<Point> points = new ArrayList<>();Point p1 = new Point();//马可以走5这个位置if((p1.x= curPoint.x-2)>=0&&(p1.y= curPoint.y-1)>=0)points.add(new Point(p1));//马可以走6这个位置if((p1.x= curPoint.x-1)>=0&&(p1.y= curPoint.y-2)>=0)points.add(new Point(p1));//马可以走7这个位置if((p1.x= curPoint.x+1)<Y&&(p1.y= curPoint.y-2)>=0)points.add(new Point(p1));//马可以走0这个位置if((p1.x= curPoint.x+2)<Y&&(p1.y= curPoint.y-1)>=0)points.add(new Point(p1));//马可以走1这个位置if((p1.x= curPoint.x+2)<Y&&(p1.y= curPoint.y+1)<X)points.add(new Point(p1));//马可以走2这个位置if((p1.x= curPoint.x+1)<Y&&(p1.y= curPoint.y+2)<X)points.add(new Point(p1));//马可以走3这个位置if((p1.x= curPoint.x-1)>=0&&(p1.y= curPoint.y+2)<X)points.add(new Point(p1));//马可以走4这个位置if((p1.x= curPoint.x-2)>=0&&(p1.y= curPoint.y+1)<X)points.add(p1);return points;}/*** 将集合中的 Point 对象根据其下一步可移动选项的个数升序排序* @param ps*/public static void sort(ArrayList<Point>ps){ps.sort(new Comparator<Point>() {@Overridepublic int compare(Point o1, Point o2) {return next(o1).size() - next(o2).size();}});}}

【尚硅谷_数据结构与算法】十二、算法相关推荐

  1. 尚硅谷Java数据结构和java算法,韩顺平数据结构和算法课后作业01

    尚硅谷Java数据结构和java算法,韩顺平数据结构和算法课后作业第一题 要求: 1)在前面的基础上,将稀疏数组保存到磁盘上,比如map.data 2) 恢复原来的数组时,读取map.data进行恢复 ...

  2. 用数组实现环形队列(尚硅谷Java数据结构与算法)

    整个代码在文章最后面,gitee地址:java数据结构与算法: 自己学习与练习数据结构的仓库https://gitee.com/ALi_L/javaDataStructurs.git 环形队列的难点如 ...

  3. 运筹学与最优化方法_[公开课]运筹学之线性规划算法十二讲

    运筹学之线性规划算法十二讲 这是最美好的时代,同样带来最无助的希望:这是最丰富多彩的时代,思想同样的匮乏:这是最互联互通的时代,一样找不到自己和同路的人:这是最光明的时代,我们依然要经过黑暗的摸索和摸 ...

  4. 马云马化腾崇拜的硅谷大神:这十二个趋势是未来致富之路(上)

    马云马化腾崇拜的硅谷大神:这十二个趋势是未来致富之路(上) 文/萌猛犸(更多内容,欢迎关注) 未来二十年乃至更长的时间,人类社会将经历怎样的发展趋势,每个人的生活方式和思想观念会变成什么样子?手机移动 ...

  5. JavaWeb学习总结——JavaWEB_Servlet(尚硅谷_佟刚老师)

    JavaWEB_Servlet(尚硅谷_佟刚老师) (本文档是在学习尚硅谷_佟刚老师的JavaWeb教学视频时写的,特此感谢) 1. 使用 JavaEE版的 Eclipse开发动态的 WEB工程(Ja ...

  6. Java尚硅谷基础笔记-day4数组-数组常见算法

    第三章 数组 3.4 数组中涉及的常见算法 3.1 数组的概述 3.2 一维数组的使用 3.3 多维数组的使用 3.4 数组中涉及的常见算法 3.5 数组工具类的使用 3.6 数组使用中的常见异常 3 ...

  7. 沃舍尔算法_[数据结构拾遗]图的最短路径算法

    前言 本专题旨在快速了解常见的数据结构和算法. 在需要使用到相应算法时,能够帮助你回忆出常用的实现方案并且知晓其优缺点和适用环境.并不涉及十分具体的实现细节描述. 图的最短路径算法 最短路径问题是图论 ...

  8. 数据结构思维 第十二章 `TreeMap`

    第十二章 TreeMap 原文:Chapter 12 TreeMap 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 这一章展示了二叉搜索树,它是个Map接口的高效实现.如果我们 ...

  9. Android项目实战之尚硅谷_硅谷p2p金融

      该项目是一年前夏天自己查阅网上资料并观看视频学习做的的android实战项目,经过这次自学android项目使自己在andorid开发的过程中收益匪浅,所以现在自己将此项目贴于网络用于自己回顾和想 ...

  10. flexbox布局_使用Flexbox制作十二列布局

    flexbox布局 演示地址 If there's one thing flexbox excels at, it's twelve-column layouts. In a twelve-colum ...

最新文章

  1. CentOs7 修改rpm安装背景图
  2. linux shell 运算符 | || () {}
  3. 使用pt-slave-delay实现mysql的延迟备份
  4. java获取数组穷举_请教一下两个数组各取一个元素生成新的数组的穷举算法设计?...
  5. 【计蒜客 - 蓝桥训练】炮台实验(数学期望,期望dp)
  6. 提供一个Android原生的Progress——SwipeToRefreshLayout下拉刷新时的等待动画
  7. 1259:【例9.3】求最长不下降序列
  8. 【InnoDB】体系结构
  9. Thread中interrupt()interrupted()和isInterrupted()的区别
  10. 推荐系统组队学习——协同过滤
  11. JavaWeb:Tomcat、Servlet
  12. MATLAB命令窗常用命令
  13. 几何公差基础知识之垂直度
  14. 基于OpenCV-python3实现证件照换背景
  15. 2021-2027全球与中国抽屉拉手市场情况与未来趋势研究报告
  16. java用zipOutputStream压缩后用WinRAR解压出现“不可预料的压缩文件末端”错误
  17. 如何制作一个vagrant的base box 及安装 additions
  18. SQL Server numeric数据类型
  19. 微信小程序下载图片到本地
  20. Android 返回键

热门文章

  1. python进阶-argparse
  2. 搭建Hadoop环境(超详细)
  3. STM32跑html协议,STM32移植SBUS协议
  4. 服装计算机辅助设计论文,计算机辅助高校服装设计论文
  5. 常用的SQL多表连接查询
  6. JavaScript学习之初识JS
  7. linux设备驱动的实现与理解
  8. spoon mysql教程_Kettle-Spoon入门示例
  9. 参加美赛能给计算机保研er带来些什么?
  10. iOS 录音功能实现