数据结构和算法基础(6)——常用十种算法

一、二分查找非递归算法

1.介绍

(1) 二分查找法只适用于从有序的数列中进行查找,将数列排序后再进行查找。
(2) 二分查找法的运行时间为对数时间O(log以2为底n的对数)。

2.代码

/**
 *  常用十种算法(一)——二分查找非递归
 */
public class BinarySearchNoRecur {

    public static void main(String[] args) {
        int [] arr = {1,3,8,10,11,67,100};
        int index = binarySearch(arr,100);
        System.out.println(index);
    }

    /**
     * 二分查找非递归实现
     * @param arr 要查找的数组
     * @param target 要查找的数
     * @return 要查找的数的下标
     */
    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) {
                right = mid - 1;
            } else if (arr[mid] < target) {
                left = mid + 1;
            } else if (arr[mid] == target){
                return mid;
            }
        }
        return -1;
    }
}

二、分治算法

1.介绍

(1) 字面上解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题。再把子问题分成更小的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题解的合并。
(2) 分治算法的应用有:二分搜索,大整数乘法,棋盘覆盖,合并排序,快速排序,线性时间选择,最接近点对问题,循坏赛日程表,汉诺塔等。
(3) 递归是一种方法,而分治是一种算法。分治只是满足了可以使用递归的条件,但是也可以使用其他方法来编写。

2.汉诺塔

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

3.汉诺塔代码

public class Hanoitower {

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

    /**
     * 汉诺塔移动方法,使用分治算法,将盘子从A移到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.println("第1个盘子从 " + a + "->" + c);
        } else {
            // 将n盘子上面n-1个盘子移动到B上
            hanoiTower(num - 1,a, c, b);
            // 把最下面的盘从A->C
            System.out.println("第" + num + "个盘子从 " + a + "->" + c);
            // 把B塔所有盘子移动到C塔上
            hanoiTower(num - 1,b, a, c);
        }
    }

}

三、动态规划(Dynamic Programming,DP)

1.介绍

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划通过迭代法自底向上求。
例如:在求解斐波那契数列过程中 ,求解fibonacci(5)求解fibonacci(5)时,得求解fibonacci(4)和fibonacci(3).其中,求解fibonacci(4)时,需要求解fibonacci(3).
在分治法中,求解完fibonacci(4)后还得重新求解fibonacci(3),即使求解fibonacci(4)子问题的时候已经求解过,也就是说,求解了二次子问题fibonacci(3).

2.01背包问题

问题描述
要求达到的目的为装入的背包的总价值最大,并且重量不超出。
要求装入的物品不能重复。
思路
每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将物品放入到背包中。即对于给定的n个物品,设w[i]为重量,v[i]为价值。v[i][j]表示在前i个物品中能装入容量为j的背包中的最大价值。则有以下结论:
(1) v[i][0] = v[0j] = 0; 第一行和第一列是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]]};

3.01背包问题代码

/**
 *  常用十种算法(三)——动态规划解决01背包
 */
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];
        // path为了记录商品放入的情况
        int[][] path = new int[n+1][m+1];
        // 初始化第一行和第一列
        for (int i = 0;i < n+1;i++) {
            v[i][0] = 0;
        }
        for(int i = 0;i < m+1;i++) {
            v[0][i] = 0;
        }

        for (int i = 1;i < v.length;i++) { // 不处理第一行
            for (int j = 1;j < v[0].length;j++) { // 不处理第一列
                if (w[i-1] > j) {
                    v[i][j] = v[i-1][j];
                } else {
                    if (v[i-1][j-w[i-1]] + val[i-1] > v[i-1][j]) {
                        v[i][j] = v[i-1][j-w[i-1]] + val[i-1];
                        path[i][j] = 1;
                    } else {
                        v[i][j] = v[i-1][j];
                    }
                }
            }
        }
        // 打印v[i][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();
        }

        // 打印path[i][j]
        for (int i =0; i < path.length;i++) {
            for (int j = 0; j < path[i].length;j++) {
                System.out.print(path[i][j] + " ");
            }
            System.out.println();
        }

        // 输出最后放入的是那些商品
        int i = path.length -1;
        int j = path[0].length-1;
        while (i > 0 && j > 0) {
            if (path[i][j] == 1) {
                System.out.println("放入了第" + i + "个物品");
                j = j - w[i -1];
            }
            i --;
        }
    }
}

四、KMP算法

1.介绍

(1) KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法。
(2) Knuth-Morris-Pratt 字符串查找算法,简称KMP算法,常用于在一个文本字符串S内查找一个模式串P的出现位置,这个算法由3个人于1977年联合发表,故取这3人的姓氏命名此算法。
(3) KMP算法通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置。
(4) 参考资料: https://www.cnblogs.com/zzuuoo666/p/9028287.html

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

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

3.字符串匹配——暴力匹配法

public static void main(String[] args) {
        String str1 = "abcdavabcsdasdas";
        String str2 = "abcm";
        System.out.println(violenceMatch(str1,str2));
    }

    public static int violenceMatch(String str1,String str2) {
        char[] s1 = str1.toCharArray();
        char[] s2 = str2.toCharArray();
        int str1Length = s1.length;
        int str2Length = s2.length;
        int i=0, j= 0;
        while(i < str1Length && j < str2Length) {
            if (s1[i] == s2[j]) {
                i++;
                j++;
            }else {
                i = i - j + 1;
                j = 0;
            }
        }
        if (j == str2Length) {
            return i - j;
        } else {
            return -1;
        }
    }

4.字符串匹配——KMP算法

	// 字串的部分匹配表,通过比较前缀和后缀相等数
    public static int[] kmpNext(String dest) {
        int[] next = new int[dest.length()];
        next[0] = 0;
        // i是后缀,j是前缀
        for (int i = 1,j=0; i < dest.length();i++) {
            while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
                j = next[j-1];
            }
            if (dest.charAt(i) == dest.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

	/**
     * 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++) {
            while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
                j = next[j - 1];
            }
            if (str1.charAt(i) == str2.charAt(j)) {
                j++;
            }
            if (j == str2.length()) {
                return i - j + 1;
            }
        }
        return -1;
    }

五、贪心算法

1.介绍

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

2.应用场景——集合覆盖问题

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

3.集合覆盖问题思路

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

3.贪心算法解决集合覆盖问题代码

/**
 *  常用十种算法(五)——K贪婪算法解决集合覆盖问题
 */
public class GreedyAlgorithm {
    public static void main(String[] args) {
        // 创建电台,放入Map中
        HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
        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("大连");

        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);
        allAreas.addAll(hashSet2);
        allAreas.addAll(hashSet3);
        allAreas.addAll(hashSet4);
        allAreas.addAll(hashSet5);

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

        // 遍历过程中,电台覆盖的地区和allAreas的交集
        HashSet<String> tempSet = new HashSet<>();
        String maxKey = null;
        int maxSize = 0;
        while (allAreas.size() != 0) {
            maxKey = null;
            maxSize = 0;
            for (String key : broadcasts.keySet()) {
                tempSet.clear();
                HashSet<String> areas = broadcasts.get(key);
                tempSet.addAll(areas);
                tempSet.retainAll(allAreas);
                if (tempSet.size() > 0 &&
                        (maxKey == null || tempSet.size() > maxSize)) {
                    maxKey = key;
                    maxSize = tempSet.size();
                }
            }
            if (maxKey != null) {
                selects.add(maxKey);
                allAreas.removeAll(broadcasts.get(maxKey));
            }
        }

        System.out.println(selects);

    }
}

4.贪心算法和动态规划的区别

(1) 贪心算法自顶向下求解,动态规划自底向上求解。
贪心自顶向上,每次向下遍历最优子树即可,这样的话就不需要知道一个节点所有子树的情况,于是构不成一棵完整的树。
动态规划自底向上,最后得到一棵完整的树,根的值即为最优值。
2 贪心最优解一定包含上一步的最优解,动态规划最优解不一定包含上一步的最优解;

贪心:每一步只考虑当前的最优解,而不考虑之前的局部最优解。下一步也只会包含上一步的最优解,而不考虑产生的子问题的最优解。
dp:原问题的结果不一定是上一步的最优解,所以要得到所有子问题的解。

3 贪心不能保证全局最优,动态规划(本质是穷举法)能保证全局最优;

4 贪心复杂度较低,动态规划复杂度较高;


六、普里姆算法(Prim)

1. 应用场景 ——修路问题

在这里插入图片描述
有7个村庄(A,B,C,D,E,F,G)现在需要修路把7个村庄连通。各个村庄的距离用边线表示权,如果修路保证各个村庄都能连通,并且总里程最短。

2. 最小生成树(Minimum Spanning Tree)

(1) 给定一个带权的无向连通图,选取一颗生成树,使树上所有边上权的总和为最小,这叫最小生成树。
(2) N个顶点,一定有N-1条边。
(3) 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法

3. Prim算法思路

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

在这里插入图片描述

4.Prim算法代码

/**
 *  常用十种算法(六)——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[][] {
                {-1,5,7,-1,-1,-1,2},
                {5,-1,-1,9,-1,-1,3},
                {7,-1,-1,-1,8,-1,-1},
                {-1,9,-1,-1,-1,4,-1},
                {-1,-1,8,-1,-1,5,4},
                {-1,-1,-1,4,5,-1,6},
                {2,3,-1,-1,4,6,-1},
        };
        MGraph mGraph = new MGraph(verxs);
        MinTree minTree = new MinTree();
        minTree.createGraph(mGraph,verxs,data,weight);
        mGraph.showGraph();
        minTree.prim(mGraph,1);
    }
}

// 最小生成树
class MinTree {
    // 创建图
    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];
            }
        }
    }

    /**
     * 编写prim算法,得到最小生成树
     * @param graph 图
     * @param v 表示从图的第几个顶点开始生成
     */
    public void prim(MGraph graph,int v) {
        // 标记节点是否被访问过
        int visited[] = new int[graph.verxs];
        // 标记当前节点为已访问
        visited[v] = 1;
        // h1和h2记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = Integer.MAX_VALUE;
        for (int k = 1; k < graph.verxs; k ++) { //已访问节点的个数,每次循环完已访问节点个数都会多一个
            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] != -1 && graph.weight[i][j] < minWeight) {
                            minWeight = graph.weight[i][j];
                            h1 = i;
                            h2 = j;
                        }
                    }
                }
            }
            System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + ">权值:" + minWeight);
            // 找到一条边最小的,将当前节点标记为已经访问
            visited[h2] = 1;
            minWeight = Integer.MAX_VALUE;
        }

    }
}

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

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

    public void showGraph() {
        for (int[] link : weight) {
            System.out.println(Arrays.toString(link));
        }
    }
}

七、克鲁斯卡尔算法(Kruskai)

1. 应用场景 ——公交站问题

在这里插入图片描述
某城市新增7个站点(A,B,C,D,E,F,G),需要修路把7个站点连通。各个站点的距离用边线表示权,问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

2. 克鲁斯卡尔算法解决公交站问题基本思路

按照权值从小到大的顺序来选择n-1条边。并保证这n-1条边不构成回路。
重点需要解决以下两个问题:
问题一 对图的所有边按照权值大小进行排序
问题二 将边添加到最小生成树中时,怎么样判断是否形成回路

3. 克鲁斯卡尔算法解决公交站问题代码

/**
 *  常用十种算法(七)——Kruskal算法
 */
public class KruskalAlgorithm {

    private int edgeNum;
    private char[] vertexs;
    private int[][] matrix;
    private static final int INF = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] vertexs = new char[]{'A','B','C','D','E','F','G'};

        int [][]weight = new int[][] {
                {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},
        };
        KruskalAlgorithm kruskalAlgorithm = new KruskalAlgorithm(vertexs, weight);
        kruskalAlgorithm.showGraph();
        kruskalAlgorithm.kruskal();
    }

    public KruskalAlgorithm(char[] vertexs,int [][] matrix) {
        int vlen = vertexs.length;
        // 初始化顶点
        this.vertexs = new char[vlen];
        this.matrix = new int[vlen][vlen];
        for (int i = 0; i < vlen; i++) {
            this.vertexs[i] = vertexs[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 kruskal() {
        int index = 0;
        int[] ends = new int[vertexs.length]; // 各顶点的终点
        EData[] rets = new EData[vertexs.length-1]; // 结果数组,保存最后的最小生成树
        EData[] edges = getEdges(); // 获取所有的边
        sortEdges(edges); // 按照权值对边进行排序

        for (int i = 0; i < edgeNum; i++) {
            // 获取一条边的两个顶点
            int p1 = getPosition(edges[i].start);
            int p2 = getPosition(edges[i].end);

            int m = getEnd(ends,p1);
            int n = getEnd(ends, p2);
            // 没构成回路
            if (m != n) {
                ends[m] = n;
                rets[index++] = edges[i];
            }
        }
        System.out.println("最小生成树为=" + Arrays.toString(rets));
    }

    /**
     * 打印邻接矩阵
     */
    public void showGraph() {
        for (int[] link : matrix) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     * 对边进行排序,冒泡排序
     */
    public 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
     */
    public int getPosition(char ch) {
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == ch) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 通过邻阶矩阵,获取图中边,放到EData[]数组中
     */
    private EData[] getEdges() {
        int index = 0;
        EData[] eData = 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) {
                    eData[index++] = new EData(vertexs[i],vertexs[j],matrix[i][j]);
                }
            }
        }
        return eData;
    }

    /**
     *  获取下标为i的顶点的终点坐标
     * @param ends 记录各个顶点对应的终点是那个
     * @param i
     * @return 下标为i的顶点的终点坐标
     */
    private int getEnd(int[] ends,int i) {
        while (ends[i] != 0) {
            i = ends[i];
        }
        return i;
    }

}

// 它的对象实例就表示一条边
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 +
                '}';
    }
}

八、迪杰斯特拉算法(Dijkstra)

1. 应用场景 ——最短路径问题

在这里插入图片描述
战争时间,有7个村庄,现在有6个邮差,从G点出发,需要分别把邮件送到A,B,C,D,E,F六个村庄。各个村庄的距离用边线表示,问:如何计算出G村庄到其他各个村庄的最短距离?如何从其他点出发到各个点的最短距离又是多少?

2. 算法思路分析

Dijkstra算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想,BFS用来求无权值的图,Dijkstra用来求有权值的图的最短路径),直到扩展到终点为止。
(1) 设置出发顶点为v,顶点集合V{v1,v2,vi…},v到V中各顶点的距离构成记录集合Dis{d1,d2,di…}(0表示到自己)。
(2) 从V中选择一个未被访问且Dis最小的顶点vi,标记该结点已被访问。
(3) 更新Dis集合,更新规则:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱结点为vi,表示是通过vi到达的)。
(4) 重复执行两步骤,直到所有结点都被访问过。

3.代码

/**
 *  常用十种算法(八)——迪杰斯特拉算法
 */
public class DijkstraAlgorithm {

    public static void main(String[] args) {
        final int INF = 65535;
        char[] vertexs = new char[]{'A','B','C','D','E','F','G'};

        int [][] matrix = new int[][] {
                {INF,5,7,INF,INF,INF,2},
                {5,INF,INF,9,INF,INF,3},
                {7,INF,INF,INF,8,INF,INF},
                {INF,9,INF,INF,INF,4,INF},
                {INF,INF,8,INF,INF,5,4},
                {INF,INF,INF,4,5,INF,6},
                {2,3,INF,INF,4,6,INF},
        };
        Graph graph = new Graph(vertexs, matrix);
        graph.showGraph();
        graph.dsj(6);
        graph.showDijkstra();
    }
}

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

    public Graph(char[] vertex,int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }

    /**
     * 打印邻接矩阵
     */
    public void showGraph() {
        for (int[] link : matrix) {
            System.out.println(Arrays.toString(link));
        }
    }

    public void dsj(int index) {
        vv = new VisitedVertex(vertex.length, index);
        update(index);
        for (int j = 1;j < vertex.length;j++) {
            index = vv.updateArr();
            update(index);
        }
    }

    // 更新index下标顶点周围顶点的距离和周围顶点的前驱顶点
    private void update(int index) {
        int len = 0;
        for (int j = 0;j < matrix[index].length;j++) {
            //出发顶点到index顶点的距离 + 从index顶点到j顶点的距离的和
            len = vv.getDis(index) + matrix[index][j];
            // 如果j没被访问过,并且len小于出发顶点到j顶点的距离,就需要跟新
            if (!vv.in(j) && len < vv.getDis(j)) {
                vv.updatePre(j,index);
                vv.updateDis(j,len);
            }
        }
    }

    public void showDijkstra() {
        vv.show();
    }
}
// 已访问顶点集合
class VisitedVertex {
    // 记录各个顶点是否访问过,1表示访问过,0表示未访问,会动态更新
    public int[] already_arr;
    // 每个下标对应的值为前一个顶点的下标,会动态更新
    public int[] pre_visited;
    // 出发顶点到其他各顶点的距离
    public int[] dis;

    /**
     * 构造器
     * @param lenght 顶点的个数
     * @param index 出发顶点的下标
     */
    public VisitedVertex(int lenght,int index) {
        this.already_arr = new int[lenght];
        this.pre_visited = new int[lenght];
        this.dis = new int[lenght];
        // 初始化dis,出了到自己的距离为0,到其他各顶点的距离为整数最大值
        Arrays.fill(dis,Integer.MAX_VALUE);
        this.already_arr[index] = 1; // 设置出发结点被访问过
        this.dis[index] = 0;
    }

    // 判断index结点是否被访问过
    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
     */
    public int getDis(int index) {
        return dis[index];
    }

    // 继续选择并返回新的访问顶点
    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;
            }
        }
        already_arr[index] = 1;
        return index;
    }

    public void show() {
        System.out.println("=========================================");
        for (int i : already_arr){
            System.out.print(i + "  ");
        }
        System.out.println();
        for (int i : pre_visited) {
            System.out.print(i + "  ");
        }
        System.out.println();
        for (int i : dis) {
            System.out.print(i + "  ");
        }
    }
}

九、弗洛伊德算法(Floyd)

1.介绍

floyd算法也是用于一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特.弗洛伊德命名。
dijkstra算法通过选定的被访问顶点,求出从出发顶点到其他顶点的最短路径。floyd算法中每个顶点都是出发访问点,求出从每一个顶点到其他顶点的最短路径。

2. 算法思路分析

(1) 设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj),Lij)。vk的取值为图中所有顶点,则可获得vi到vj的最短路径。
(2) 至于vi到vk的最短路径Ljk或者vk到vj的最短路径Lkj,是以同样的方式获得。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
要想更新如上两张表,需要借助如下图3个列表:
在这里插入图片描述
最外层循环是中间顶点,第二层循环是出发顶点,最内层循环时终点。每次循环时通过步骤(1)来比较更新两个表。

3.代码

/**
 *  常用十种算法(九)——弗洛伊德算法
 */
public class FloydAlgorithm {
    public static void main(String[] args) {
        final int INF = 65535;
        char[] vertex = {'A','B','C','D','E','F','G'};
        int [][] matrix = new int[][] {
                {0,5,7,INF,INF,INF,2},
                {5,0,INF,9,INF,INF,3},
                {7,INF,0,INF,8,INF,INF},
                {INF,9,INF,0,INF,4,INF},
                {INF,INF,8,INF,0,5,4},
                {INF,INF,INF,4,5,0,6},
                {2,3,INF,INF,4,6,0},
        };
        FloydGraph floydGraph = new FloydGraph(vertex.length, matrix, vertex);
        floydGraph.floyd();
        floydGraph.show();
    }
}

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

    /**
     *
     * @param length 结点个数
     * @param matrix 邻接矩阵
     * @param vertex 结点数组
     */
    public FloydGraph(int length,int [][] matrix,char[] vertex) {
        this.vertex = vertex;
        this.dis = matrix;
        this.pre = new int[length][length];
        // 初始化前驱顶点数组
        for (int i = 0; i < length; i++) {
            Arrays.fill(pre[i],i);
        }
    }

    // 佛洛伊德算法
    public void floyd() {
        int len = 0;
        // 中间顶点遍历
        for (int k = 0;k < dis.length; k++) {
            // 出发顶点遍历
            for (int i = 0;i < dis.length;i++) {
                // 终点遍历
                for (int j = 0;j < dis.length;j++) {
                    len = dis[i][k] + dis[k][j]; // 求出出发顶点到中间顶点的距离,加上中间顶点到终点的距离
                    if (len < dis[i][j]) { // 如果len小于出发顶点到终点的距离
                        dis[i][j] = len;
                        pre[i][j] = pre[k][j];
                    }
                }
            }
        }
    }

    // 显示pre和dis
    public void show() {
        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();
            System.out.println("-----------------------------------------");
        }
    }
}

十、骑士周游问题

1.介绍

(1) 马踏棋盘算法也称为骑士周游问题。
(2) 将马随机放在国际象棋8*8期盼的某个方格中,马按照象棋规则进行移动。要求每个方格只能进入一次,走遍棋盘上全部64个方格。
在这里插入图片描述

2. 算法思路分析

(1)创建棋盘chessBoard,是一个二维数组
(2)将当前位置设置为已经访问,然后根据当前位置,计算马还能走那些位置,并放入到一个集合中(ArrayList),最多有8个位置。
(3)遍历ArrayList中存放的所有位置,看那个可以走通,如果走通,就继续走,走不通就回溯。
(4)判断马是否完成了任务,使用step和应走步数比较。如果没达到,则没有完成任务,将整个棋盘置0。
注意:马不同的走法,会得到不同的结果,效率也会有影响。
贪心算法优化: 对下一步还能走的位置的集合中每个点的下一步集合数目,进行非递减排序。

3.代码

/**
 *  常用十种算法(十)——骑士周游算法/马踏棋盘算法
 */
public class HorseChessboard {

    private static int X; // 棋盘的列
    private static int Y; // 棋盘的行
    private static boolean visited[]; // 标记棋盘各个位置是否被访问过
    private static boolean finished; // 标记棋盘的所有位置都被访问过

    public static void main(String[] args) {
        X = 8;
        Y = 8;
        int row = 1; // 初始行
        int column = 1; // 列
        int[][] chessboard = new int[Y][X];
        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();
        }
    }

    /**
     * 骑士周游问题算法
     * @param chessboard 棋盘
     * @param row 马的当前位置的行
     * @param column 马的当前位置的列
     * @param step 是第几步,初始位置就是第一步
     */
    public static void traversalChessboard(int[][] chessboard,int row,int column,int step) {
        chessboard[row][column] = step;
        visited[row*X + column] = true;
        ArrayList<Point> ps = next(new Point(column, row)); // 获取当前位置可以走的下一个位置的集合
        sort(ps); // 贪心算法优化
        while (!ps.isEmpty()) {
            Point p = ps.remove(0);
            // 判断该点是否已经被访问过
            if (!visited[p.y*X + p.x]) {
                traversalChessboard(chessboard,p.y,p.x,step + 1);
            }
        }
        if (step < X*Y && !finished) {
            chessboard[row][column] = 0;
            visited[row*X+column] = false;
        } else {
            finished = true;
        }
    }

    public static void sort(ArrayList<Point> ps) {
        ps.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                int count1 = next(o1).size();
                int count2 = next(o2).size();
                if (count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return 1;
                }

            }
        });
    }

    /**
     * 根据当前位置(Point对象),计算马还能走那些位置,并放入到一个集合中
     * @param curPoint
     * @return
     */
    public static ArrayList<Point> next(Point curPoint){
        ArrayList<Point> ps = new ArrayList<>();
        Point p1 = new Point();
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) { // 5
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y -2) >= 0) { // 6
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y -2) >= 0) { // 7
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y -1) >= 0) { // 0
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y +1) < Y) { // 1
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y +2) < Y) { // 2
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) { // 3
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y +1) < Y) { // 4
            ps.add(new Point(p1));
        }
        return ps;
    }

}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值