常用的十种算法

十种算法

1、二分查找算法(非递归)

1、介绍:

1)二分查找只适用于有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找

2)二分查找算法的运行时间为对数时间O(log2 n)

public class BinarySearchNoRecur {
    public static void main(String[] args) {
        // 测试
        int[] arr={1,3,8,10,11,67,110};
        int index = binarySearch(arr,11);
        System.out.println("index="+index); // 0

    }

    // 二分查找的非递归实现
    /*
    * arr 待查找数组
    * 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){
                left=mid+1;
            }else {
                right=mid-1;
            }
        }
        return -1;
    }
}


2、分治算法

1、介绍:

分治法就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题…直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅里叶变换

分治算法的基本步骤:

分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。

解决:若子问题规模较小而容易被解决则直接解,否则递归的解各个子问题

合并:将各个子问题的解合并为原问题的解。

分治算法最佳实践-汉诺塔

思路分析:
1)如果有一个盘,a-c

如果有n>=2情况,总是可以看做是两个(盘1最下边的盘)、(盘2上面的盘)

①先把上面的盘a-b
② 把最下面的盘a-c
③ 再把b的塔 实现b-c

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

    // 汉诺塔移动方法
    // 分治算法
    public static void hanoiTower(int num,char a,char b,char c){
        if (num==1){
            System.out.println("第1个盘从"+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);
        }
    }
}


3、动态规划算法

1、介绍:

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解

动态规划可以通过填表的方式来逐步推进,得到最优解

应用场景举例- - -背包问题

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

吉他 重量1磅 价格1500
音响 重量4磅 价格3000
电脑 重量3磅 价格2000

1)要求达到的目标为装入的背包的总价值最大,并且重量不超出容量
2)要求装入的物品不能重复

思路分析和图解
① 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
② 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

算法的主要思想

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

1)v[i][0]=v[0][j]=0; // 前i个物品对于背包容量为0的最大价值是0;前0个物品对于背包容量为j的最大价值是0

2)二维数组的添加是从第二行第二列开始遍历的故i=1、j=1

i从1开始,第一个商品(i-1)的重量就是w[i-1]、第一个商品(i-1)的价值就是val[i-1]

当w[i-1]>j时:v[i][j]=v[i-1][j]
当i商品的重量大于当前背包的容量j时,(i 这个物品就放不进去) - - - 把 v[i-1][j](前i-1件装入容量为j的背包的最大价值)赋给 v[i][j] 因为i放不进去

3)当j>=w[i-1]时,v[i][j]=Math.max(v[i-1][j],v[i-1][j-w[i-1]]+val[i-1])

当i商品的重量小于等于当前背包的容量j时,(i 这个物品可以放进背包) - - - v[i][j](前i件装入容量为j的背包的最大价值),在取v[i-1][j]和v[i-1][j-w[i-1]]+val[i-1]装入i后,剩余容量为j-w[i],在此剩余容量中装入前i-1件商品的最大价值+装入i的价值 v[i])取两者中最大价值的一个赋给 v[i][j]。

在这里插入图片描述

public class dynamic {
    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; //物品的个数

        // 为了记录放入商品的情况,定义一个二维数组
        int[][] path=new int[n+1][m+1];

        //创建二维数组
        // v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值
        int[][] v= new int[n+1][m+1];

        // 初始化第一行、第一列,不去设置也可以默认为0
        for (int i=0;i<v.length;i++){
            v[i][0]=0;  // 将第一列设置为0
        }
        for (int i=0;i<v[0].length;i++){
            v[0][i]=0;  // 将第一行设置为0
        }

        // 根据前面公式来动态规划处理
        for (int i=1;i<v.length;i++){   //不处理第一行,i从1开始
            for (int j=1;j<v[i].length;j++){  //不处理第一列,j从1开始
                //公式
                if (w[i-1]>j){  // 因为程序i是从1开始的,因此原公式w[i]改为
                                // w[i-1]
                    v[i][j]=v[i-1][j];
                }else {  // i从1开始,按顺序商品价格和重量在数组中val[i-1]、w[i-1]
                    //v[i][j]=Math.max(v[i-1][j],val[i-1]+v[i-1][j-w[i-1]]);
                    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;   // 此记录的是最优解,i表示第几个商品
                    }else {
                        v[i][j]=v[i-1][j];
                    }

                }
            }
        }

        // 输出一下动态规划后的二维数组
        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){ //说明第i个商品放入了容量为j的背包中
                System.out.printf("第%d个商品放入到背包\n",i);
                j-=w[i-1]; // 减去当前i商品的重量w[i-1]
            }
            i--;
        }

    }


4、KMP算法(串的匹配算法)

1、介绍:

定位操作:若主串str1中存在与串str2值相同的子串,则返回它在主串str1中第一次出现的位置;否则函数返回0.

2、暴力匹配算法主串和模式串都会回溯
在这里插入图片描述

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 i=0,j=0;  //i索引指向s1,j索引指向s2

        while (i<s1.length && j< s2.length){
            if (s1[i]==s2[j]){  // 匹配成功
                i++;        // 继续比较后续字符
                j++;
            }else {
                            // 检查下一个字串
                i=i-(j-1);  // i和j同增,匹配失败,把i退回上次位置的下一下位置
                j=0;
            }
        }
        if(j==s2.length){
            return i-j;   //如果全部匹配(最后匹配成功j++)索引会等于s2.length,匹配成功
        }else {         // 匹配失败
            return 0;
        }
    }
}

3、KMP算法主串指针不回溯,模式串回溯

思路分析:

① 先得到字串的部分匹配表

② 使用部分匹配表完成KMP匹配


《部分匹配表》

先介绍前缀和后缀

例如:字符串 “bread”

前缀(字符串中包含第一个元素但不包含最后一个元素的连续子字符串):b 、br、bre、brea

后缀(字符串中包含最后一个元素但不包含第一个元素的连续子字符串): read、ead、ad、d


部分匹配值”就“前缀”和“后缀”的**最长**的共有元素的长度

例如:

“A”的前缀和后缀都只能是空集,最长共有元素的长度为0

“ABC”的前缀[A,AB],后缀[C,BC],最长共有元素的长度为0

"ABCDAB"的前缀[A,AB,ABC,ABCD],后缀[B,AB,DAB,CDAB,BCDAB],最长共有元素为“AB”,长度是2

在这里插入图片描述
主串和模式串的下标索引都是从0开始

next数组定义:当主串与模式串的某一位字符不匹配时,模式串要回退的位置。

j表示模式串的第j个字符匹配失败时
next[j]:其值 = 第j位字符前面j-1位字符组成的子串的前后缀重合字符数 表示模式串跳到next[j]位置再继续匹配

子串前后缀最大重合长度为j,如果子串增加一个元素,如果此元素Str[i]与Str[j](Str[1])相等,则增加元素后的子串的最大重合长度j也+1

不相等则,令j退回到next[j](j=next[j]),再判断此元素与Str[i]与新的Str[j]是否相等…(循环判断)

图解如下相同颜色框的表示是元素相对应的子串
在这里插入图片描述

 // 甩手掌柜凡三岁 求next数组   运用原理与上面两者不同
    public static int[] KmpNext(String str){    // 求模式串的Next数组
        int[] next = new int[str.length()];   // 创建一个模式串长度的next数组
        int j=-1;         //初始模式串子串前后缀最大重合长度为-1
        int i=0;            // 从模式串第一个元素(i=0)开始遍历
        next[0]=-1;         // 定义模式串i=0位置元素之前的子串的前后缀最大重合长度为-1
        while (i<str.length()-1){ // 相当于上面两种的移位,故最后一个的不需要
            if (j==-1 || str.charAt(i)==str.charAt(j)){  // i从0开始,这里j的值第一次出现是i前一位对应的前后缀最大重合长度
                next[++i] = ++j;
            }else {             //如果不相等 把j移回到next[j]位置,
                                // 再进循环判断,
                j=next[j];
            }
        }
        return next;
    }

在这里插入图片描述
优化代码如下:

public static int[] KmpNext(String str){    // 求模式串的Next数组
        int[] next = new int[str.length()];   // 创建一个模式串长度的next数组
        int j=-1;         //初始模式串子串前后缀最大重合长度为-1
        int i=0;            // 从模式串第一个元素(i=0)开始遍历
        next[0]=-1;         // 定义模式串i=0位置元素之前的子串的前后缀最大重合长度为-1
        while (i<str.length()-1){ // 相当于上面两种的移位,故最后一个的不需要
            if (j==-1 || str.charAt(i)==str.charAt(j)){  // i从0开始,这里j的值第一次出现是i前一位对应的前后缀最大重合长度
               i++;j++;
               if(str.charAt(i)==str.charAt(j)){
					next[i]=next[j];
					}
				else{
				next[i]=j;
				}
            }else {             //如果不相等 把j移回到next[j]位置,
                                // 再进循环判断,
                j=next[j];
            }
        }
        return next;
    }

KMP算法查找

加粗样式

// KMP算法
    public static int IndexKmp(String str1,String str2){
        char[] s1=str1.toCharArray();
        char[] s2=str2.toCharArray();

        int i=0,j=0;  //i索引指向s1,j索引指向s2,也是next数组next[j]对应查找的最大重合长度(j返回的位置)

        int [] next=KmpNext2(str2); //获取模式串str2的next数组
        while (i< s1.length && j< s2.length){
            if (j==-1 || s1[i]==s2[j]){
                i++;
                j++;
            }else {
                j=next[j];    // 匹配失败j移回到其对应的next数组应该的位置
            }
        }
        if (j == s2.length){   // 匹配到最后一个(s2.length-1)也成功j++后
                            // j==s2.length  全部匹配成功
            return i-j;   // 返回第一次匹配成功的位置
        }else {
            return 0;    // 返回0则匹配失败
        }
    }


5、贪心算法

1、介绍

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

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


贪心算法最佳应用- - -集合覆盖

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

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

思路分析:

1)遍历所有广播电台,找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已经覆盖的地区,但没有关系)

2)将这个电台加入到一个集合中(比如ArrayList),想办法把该电台覆盖的地区在下一次比较时去掉。

3)重复第一步直到覆盖了全部的地区。

public class GreedyAlgorithm {
    public static void main(String[] args) {
        // 创建广播电台,放入到一个Map中  key不能重复,value没有限制
        HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
        // 将各个电台放入到broadcasts
        HashSet<String> hashSet1 = new HashSet<>();
        hashSet1.add("北京");
        hashSet1.add("上海");
        hashSet1.add("天津");

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

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

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

        HashSet<String> hashSet5 = new HashSet<>();
        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);

        // 存放所有地区
        HashSet<String> allAreas = new HashSet<>();
        allAreas.addAll(hashSet1);  // addAll添加全部,自动去重
        allAreas.addAll(hashSet2);
        allAreas.addAll(hashSet3);
        allAreas.addAll(hashSet4);
        allAreas.addAll(hashSet5);

        /*allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");*/

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

        //定义一个临时的集合,在遍历过程中,存放各个电台与剩余未覆盖地区的交集
        HashSet<String> tempSet = new HashSet<>();

        // 定义一个maxKey保存在一次遍历中,能够覆盖最多未覆盖地区对应的电台的key
        // 如果maxKey(存放key 即电台)不为null,则会加入到selects
        // len存放遍历过后 电台与未覆盖地区交集元素的最大个数
        String maxKey=null;
        int len=0;

        while (allAreas.size()!=0){  // 如果allAreas部位0,则表示还有未覆盖的地区

            // 没进行一次while,把上一次存放电台的maxKey置空,同时len也置零
            maxKey=null;
            len=0;

            // 遍历broadcasts,取出对应的key
            for (String key:broadcasts.keySet()){
                // 每进行一次for,就要把tempSet清空,
                //              以备存下一个电台的元素进行交集运算
                tempSet.clear();

                //当前key能够覆盖的地区
                HashSet<String> areas=broadcasts.get(key);
                tempSet.addAll(areas);  //放入临时集合中
                // 求出stmpSet 和allAreas集合的交集,交集重新赋给tempSet
                tempSet.retainAll(allAreas);

                // templen存放本轮中交集个数
                int templen=tempSet.size();

                // 如果当前电台与未覆盖的地区集合有交集
                //  且 ( maxKey没有被赋值
                //  或  当前交集元素个数大于上一轮电台与未覆盖地区集合的交集元素的个数
                //                   templen>len   体现出贪心算法的特点
                if (templen >0 && (maxKey==null || templen>len)){
                //if (tempSet.size() >0 && (maxKey==null || tempSet.size()>broadcasts.get(maxKey).size())){
                    maxKey = key;    // 把电台更换为当前电台
                    len = templen;   //就把当前交集个数赋给len
                }
            }
            //maxKey不为null,就应该将maxKey加入到selects
            if (maxKey != null){
                selects.add(maxKey);
                //将maxKey指向的广播电台覆盖的地区从allAreas中去掉
                allAreas.removeAll(broadcasts.get(maxKey));
            }

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


6、Prim(普里姆)算法 - - -即解决(带权连通无向图的最小生成树


最小生成树(MST)的介绍:

 1)给定一个带权的无向连通图,如何选取一颗生成树,使树上所有边上权的总和为最小,这叫最小生成树。
 
 2)最小生成树可能有多个,但边的权值之和总是唯一且最小的

 3)最小生成树的边数 = 顶点数 -1. 砍掉一条则不连通,增加一条边则会出现回路。(极小连通子图)

 4)如果一个连通图本身就是一颗树,则其最小生成树就是它本身

 5) 只有连通图才有生成树,非连通图只有生成森林

求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法


应用场景-修路问题带权无向连通图

在这里插入图片描述
1)一个乡镇,有7个村庄(A、B、C、D、E、F、G),现在需要修路把7个村庄连通(上图的线是修路的长度 权值)

2)各个村庄的距离用边线表示(权),比如A-B距离5公里

3)问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短


思路分析

从某个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

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之间存在边

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[][]{   //999表示不连通,其他的表示连通的权
                {999,5,7,999,999,999,2},
                {5,999,999,9,999,999,3},
                {7,999,999,999,8,999,999},
                {999,9,999,9,999,4,999},
                {999,999,8,999,999,5,4},
                {999,999,999,4,5,999,6},
                {2,3,999,999,4,6,999},
        };

        // 创建MGraph对象
        MGraph graph = new MGraph(verxs);
        // 创建一个MinTree对象
        MinTree minTree = new MinTree();
        minTree.createGraph(graph,verxs,data,weight);
        // 显示
        minTree.showGraph(graph);

        //测试prim
        minTree.prim(graph,0);
    }
}

// 先创建村庄的图->再求出最小生成树
class MinTree {
    // 创建图的邻接矩阵
    /*
     *  graph  图对象
     *  verxs  图对应的顶点个数
     *  data   图的各个顶点的值
     *  weight 图的邻接矩阵
     * */
    public void createGraph(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算法,得到最小生成树
    /*
    *  graph 村庄图
    *   v表示从图的第几个顶点开始生成'A'->0 'B'->1。。。
    * */
    public void prim(MGraph graph, int v) {
        // visited[]标记结点(顶点)是否被访问过
        int[] visited = new int[graph.verxs];
        //visited[]初始默认都为0,表示没有被访问
        /*for (int i=0;i< graph.verxs;i++){
            visited[i]=0;
        }*/

        // 把当前这个顶点标记为已访问
        visited[v]=1;
        // 最后h1和h2记录的是当前权值最小边连接的两个顶点的下标
        int h1=-1;
        int h2=-1;
        int minWeight=999;  // 将minWeight初始成一个大数,后面遍历过程会被替换
        for (int k=1;k<graph.verxs;k++){ //找到graph.verxs-1条边
                        // 普里姆算法结束时,边的个数比顶点少1,故从1开始
            // 确定每一次生成的子图,距离最近
            for (int i=0;i<graph.verxs;i++){    // 遍历已经访问过的顶点
                if(visited[i]==1){
                    for (int j=0;j<graph.verxs;j++){  //遍历没有访问的顶点
                        if( 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=999;
            minWeight=999;
        }
    }

}

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

    public MGraph(int verxs) {
        this.verxs = verxs;
        data = new char[verxs];
        weight = new int[verxs][verxs];

    }
}


7、Kruskal(克鲁斯卡尔)算法 - - -即解决(带权连通无向图的最小生成树) 适用于边稀疏的网的最小生成树


应用场景-公交站问题

在这里插入图片描述
1)某城市新增7个站点(A、B、C、D、E、F、G),现在需要修路把7个站点连通

2)各个站点的距离用边线表示(权),比如A-B距离12公里

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


思路分析

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有点但都连通

① 按照权值从小到大的顺序选择n-1条边,保证这n-1条边不构成回路(处理方式:记录顶点再最小生成树中的终点,顶点的终点是在最小生成树中与它连通的最大顶点(对编号来说)。然后每次需要将一条边添加到最小生成树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路)。
加入的边的两个顶点不能都指向同一个终点,否则会构成回路

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

public class KruskalAlgorithm {

    private int edgeNum;//边的个数
    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[][] = {
                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
                /*A*/ {   0,  12, INF, INF, INF,  16,  14},
                /*B*/ {  12,   0,  10, INF, INF,   7, INF},
                /*C*/ { INF,  10,   0,   3,   5,   6, INF},
                /*D*/ { INF, INF,   3,   0,   4, INF, INF},
                /*E*/ { INF, INF,   5,   4,   0,   2,   8},
                /*F*/ {  16,   7,   6, INF,   2,   0,   9},
                /*G*/ {  14, INF, INF, INF,   8,   9,   0}};


        //创建KruskalCase 对象实例
        KruskalAlgorithm kruskal = new KruskalAlgorithm(vertexs, matrix);
        //输出构建的
        kruskal.print();

        //
        kruskal.kruskal();

    }

    //构造器
    public KruskalAlgorithm(char[] vertexs,int[][] matrix){
        //初始化顶点数和边的个数
        int vlen=vertexs.length;

        //初始化顶点  复制拷贝的方式赋值,值的变化不改变引用类型的vertexs本身
        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) { //不等于最大值,说明此边存在
                    edgeNum++;
                }
            }
        }


    }

    //打印邻接矩阵
    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("%12d", 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 tmp = edges[j];
                    edges[j] = edges[j+1];
                    edges[j+1] = tmp;
                }
            }
        }
    }

    /**
     *
     * @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[edgeNum];
        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) { // i = 4 [0,0,0,0,5,0,0,0,0,0,0,0]
        while(ends[i] != 0) {
            i = ends[i];
        }
        return i;
    }


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

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

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

        //遍历edges 数组,将边添加到最小生成树中时,判断是准备加入的边否形成了回路,如果没有,就加入 rets, 否则不能加入
        for(int i=0; i < edgeNum; i++) {
            //获取到第i条边的第一个顶点(起点)
            int p1 = getPosition(edges[i].start); //p1=4
            //获取到第i条边的第2个顶点
            int p2 = getPosition(edges[i].end); //p2 = 5

            //获取p1这个顶点在已有最小生成树中的终点
            int m = getEnd(ends, p1); //m = 4
            //获取p2这个顶点在已有最小生成树中的终点
            int n = getEnd(ends, p2); // n = 5
            //是否构成回路
            if(m != n) { //没有构成回路
                ends[m] = n; // 设置m 在"已有最小生成树"中的终点 <E,F> [0,0,0,0,5,0,0,0,0,0,0,0]
                rets[index++] = edges[i]; //有一条边加入到rets数组
            }
        }
        //<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]);
        }
    }



}


// 创建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;
    }

    @Override
    public String toString() {
        return "EData{" +
                "start=" + start +
                ", end=" + end +
                ", weight=" + weight +
                '}';
    }

}


8、Dijkstra(迪杰斯特拉)算法 - - -即解决(一个顶点到其他顶点的最短路径) - - - 不适用于带负权图

最短路径问题:

1、单源最短路径(一个顶点到其他顶点最短路径)

① BFS算法(无权值)
② Dijkstra算法(带权图、无权图)

2、各顶点间的最短路径

Floyd算法(带权图、无权图)

应用场景-最短路径问题

在这里插入图片描述
1)战争时期,胜利乡有7个村庄(A、B、C、D、E、F、G),现在有六个邮差,从G点出发,需要分别把邮件分别送到A、 B、C、D、E、F六个村庄

2)各个村庄的距离用边线表示(权)

3)问:如何计算出G村庄到其他各个村庄的最短距离

4)如果从其他点出发到各个点的最短距离又是多少

算法过程:

设置出发顶点为v,顶点集合V{v1,v2,vi…},v到V中各顶点的距离构成距离集合Dis,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)重复执行两步操作,直到最短路径顶点为目标顶点即可结束。
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 =666; //表示不可连接
        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 visitedVertex; // 已经访问过顶点的集合

    public Graph(char[] vertex,int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }
    // 显示图
    public void showGraph(){
        for(int[] link:matrix){
            System.out.println(Arrays.toString(link));
        }
    }

    //显示最后结果
    public void showDijkstra(){
        visitedVertex.show();
    }

    //迪杰斯特拉算法
    public void dsj(int index){
        visitedVertex = new VisitedVertex(vertex.length, index);
        update(index);  // 更新index顶点到周围顶点的距离和前驱顶点

        for (int j=1;j<vertex.length;j++){
            index=visitedVertex.updateArr(); //选择并返回新的访问顶点
            update(index);          // 更新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顶点的距离的和
            //      (即:出发顶点到j距离)
            len=visitedVertex.getDis(index) + matrix[index][j];
            // 如果j顶点没有被访问过,并且len小于出发顶点到j顶点的距离,就需要更新
            if(!visitedVertex.in(j) && len<visitedVertex.getDis(j)){
                visitedVertex.updatePre(j,index); // 当前访问的index顶点就是j的前驱
                visitedVertex.updateDis(j,len);  // 把顶点到j的距离就更新为len
            }
        }
    }

}

// 已访问顶点集合
class VisitedVertex{
    //记录各个顶点是否访问过,1表示访问过,0表示未访问,会动态更新
    public int[] already_arr;

    //每个下标对应的值为前一个顶点下标(前驱),会动态更新
    public int[] pre_visited;

    //记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其他顶点的距离,
    // 会动态更新,求的最短距离就会存放到dis
    public int[] dis;

    //构造器
    /*
    * length:表示顶点个数
    * 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];

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

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

    // 更新出发顶点到index顶点的距离
    public void updateDis(int index ,int len){
        dis[index]=len;
    }

    // 更新顶点pre的前驱顶点为index结点
    public void updatePre(int pre,int index){
        pre_visited[pre]=index;
    }

    // 返回出发顶点到index顶点的距离
    public int getDis(int index){
        return dis[index];
    }

    // 继续选择并返回新的访问顶点,
    // 比如这里的G被访问完后,就是A点作为新的访问顶点(注意不是出发顶点)
    public int updateArr(){
        int min =666,index = 0;
        for (int i=0;i<already_arr.length;i++){    // 广度优先遍历+贪心
            if (already_arr[i]==0 && dis[i]<min){
                            // i顶点未被访问,且i与当前顶点距离小于666(即无穷)
                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!=666){
                System.out.print(vertex[count]+"("+i+")");
            }else {
                System.out.println("N ");
            }
            count++;
        }
        System.out.println();
    }
}


8、Floyd(弗洛伊德)算法 - - -即解决(各个顶点间的最短路径)

算法过程

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,是以同样的方式获得

在这里插入图片描述
1)胜利乡有7个村庄(A、B、C、D、E、F、G)

2)各个村庄的距离用边线表示(权)

3)问:如何计算出各村庄到其他各村庄的最短距离?

在这里插入图片描述

算法核心在于各顶点之间距离表、顶点前驱关系表

// ...准备工作,根据图的信息初始化矩阵A和path
for(int k=0;k<n;k++){    		// 考虑以Vk作为中转点
	for(int i=0;i<n;i++){		// 遍历整个矩阵,i为行号,j为列号
		for(int j=0;j<n;j++){
			if(A[i][j]>A[i][k]+A[k][j]){  // 以VK为中转点的路径更短
				A[i][j]=A[i][k]+A[k][j];	// 更新最短路径长度
				path[i][j]=k; 				// 前驱表对应的更新为中转点
			}
		}
	}
}

代码如下:

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 =999;
        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};

        //创建FloydGraph对象
        FloydGraph floydGraph = new FloydGraph(vertex.length, matrix, vertex);

        // 调用floyd
        floydGraph.floyd();

        floydGraph.show();
    }
}


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

    // length 顶点个数  matrix 邻接矩阵  vertex 顶点数组
    public FloydGraph(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() {
        for (int k = 0; k < dis.length; k++) {
            // 输出pre每行
            for (int i = 0; i < dis.length; i++) {
                System.out.print(vertex[pre[k][i]] + "   ");
            }
            System.out.println();
        }
        System.out.println("====================");

        for (int k = 0; k < dis.length; k++) {
            // 输出dis每行
            for (int i = 0; i < dis.length; i++) {
                System.out.printf(" %2d ",dis[k][i] );
            }
            System.out.println();
        }

    }

    // 弗洛伊德算法
    public void floyd(){
        //从中转顶点遍历,k就是中转顶点的坐标
        for(int k=0;k<dis.length;k++){    		// 考虑以Vk作为中转点
            for(int i=0;i<dis.length;i++){		// 遍历整个矩阵,i为行号,j为列号
                for(int j=0;j<dis.length;j++){
                    if(dis[i][j]>dis[i][k]+dis[k][j]){  // 以VK为中转点的路径更短
                        dis[i][j]=dis[i][k]+dis[k][j];	// 更新最短路径长度
                        pre[i][j]=pre[k][j]; 				// 前驱表对应的更新为中转点
                    }
                }
            }
        }
    }
    
}


10、马踏棋盘算法(骑士周游问题) - - -图的深度优先遍历BFS

介绍

1)将马随即放在国际象棋的8*8棋盘Board[0-7][0-7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上64个方格。

在这里插入图片描述

代码实现 — 含贪心优化

public class HorseChessBoardAlgorithm {

    private static int X; //表示棋盘的列数(X的下标负责每行的左右)
    private static int Y; //表示棋盘的行数(Y的下标负责每列的左右)
    //创建一个数组,标记棋盘的各个位置是否被访问过,一维表示二维
    private static boolean visited[];
    // 使用一个属性,标记是否棋盘的所有位置都被访问
    private static boolean finished; // 初始为flase,如果为true,表示成功


    public static void main(String[] args) {
        //测试
        System.out.println("骑士周游:");
        X = 8;
        Y = 8;
        int row = 1; //骑士初始行,从1开始编号
        int col = 1; //骑士初始列,从1开始编号
        //创建棋盘
        int[][] chessboard = new int[X][Y];
        //初始值都为false
        visited = new boolean[X * Y];
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard, row -1, col - 1, 0);
        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();
        }
    }

    /**
     * 骑士周游算法
     * @param chessboard 棋盘(棋盘的每个格子记录的是骑士第几部访问的该位置)
     * @param row 骑士当前位置的行
     * @param col 骑士当前位置的列
     * @param step 当前骑士走的是第几步,初始时为1
     */
    public static void traversalChessboard(int[][] chessboard, int row, int col, int step){
        //记录骑士第step步访问该位置
        chessboard[row][col] = step;
        //将该位置记录为已访问  一维表示二维
        visited[row * X + col] = true;
        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> next = next(new Point(col, row));
        //对next进行排序  排序的规则就是对next的所有的Point对象的下一步的位置的数目,进行非递减排序
        sort(next);  // 贪心优化

        while (!next.isEmpty()){
            //取出骑士下一个走的位置
            Point p = next.remove(0);
            //判断下一个位置是否已经访问过,没有访问则访问
            if(!visited[p.y * X + p.x]){
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }

        //判断骑士是否完成周游,如果骑士走的步数小于 X * Y 则表示没有完成任务,此时,将当前位置,置0
        //step < X * Y有两种情况:1.棋盘还没有走完  2.棋盘此时正在回溯
        if(step < X * Y - 1  && !finished){
            chessboard[row][col] = 0;
            visited[row * X + col] = false;
        }else {
            finished  = true;
        }

    }

    //根据当前这一步的所有的下一步的选择位置,进行非递减排序,减少回溯的次数
    private static void sort(ArrayList<Point> next) {
        next.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;
                }
            }
        });
    }

    /**
     * 根据当前的位置(Point为java的内置对象),计算骑士下一步能走的位置,最多有8个位置
     * @param curPoint
     * @return 包含当前位置能访问的下一个位置的坐标!!!
     *      (坐标横着的是X轴!!!),X的下标自左向右增大,Y的下标自上向下增大
     */
    public static ArrayList<Point> next(Point curPoint){
        ArrayList<Point> nextList = new ArrayList<>();
        Point p = new Point();

        //表示骑士可以走5这个位置
        if((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y - 1) >= 0){
            nextList.add(new Point(p));
        }

        //表示骑士可以走6这个位置
        if((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y - 2) >= 0){
            nextList.add(new Point(p));
        }

        //表示骑士可以走7这个位置
        if((p.x = curPoint.x + 1) < X && (p.y = curPoint.y - 2) >= 0){
            nextList.add(new Point(p));
        }

        //表示骑士可以走0这个位置
        if((p.x = curPoint.x + 2) < X && (p.y = curPoint.y - 1) >= 0){
            nextList.add(new Point(p));
        }

        //表示骑士可以走1这个位置
        if((p.x = curPoint.x + 2) < X && (p.y = curPoint.y + 1) < Y){
            nextList.add(new Point(p));
        }

        //表示骑士可以走2这个位置
        if((p.x = curPoint.x + 1) < X && (p.y = curPoint.y + 2) < Y){
            nextList.add(new Point(p));
        }

        //表示骑士可以走3这个位置
        if((p.x = curPoint.x - 1) >= 0 && (p.y = curPoint.y + 2) < Y){
            nextList.add(new Point(p));
        }

        //表示骑士可以走4这个位置
        if((p.x = curPoint.x - 2) >= 0 && (p.y = curPoint.y + 1) < Y){
            nextList.add(new Point(p));
        }

        return  nextList;
    }

}
  • 4
    点赞
  • 157
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值