数据结构(十九)——常用的算法(三)

前言

  我们上篇文章给大家介绍了KMP算法、贪心算法、prim算法和kruskal算法,详细给大家介绍了各大算法常用的场景以及算法的思路和算法在具体应用中应该如何去用代码实现。本文给大家介绍Dijkstra算法、Folyd算法以及最后的马踏棋盘算法。首先给大家介绍Dijkstra算法的相关内容。

一、Dijkstra算法

1、最短路径问题

  看一个应用场景和问题

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

2、Dijkstra算法过程

  Dijkstra算法是典型最短路径算法,用于计算一个结点到其它结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。接下来给大家介绍Dijkstra算法的过程。

  • 1、设置出发顶点为v,顶点集合V{v1,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、重复执行两步骤,直到最短路径顶点为目标顶点即可结束。

  我们通过Dijkstra算法来解决前面提到的最短路径问题。首先通过图解的方式来介绍Dijstra算法的过程:

  最后我们用java代码实现Dijstra算法在最短路径中的应用:

import java.util.Arrays;

public class DijkstraAlgorithm {
    
    

    public static void main(String[] args) {
    
    
        char[] vertex = {
    
     'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        //邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;// 表示不可以连接
        matrix[0]=new int[]{
    
    N,5,7,N,N,N,2};
        matrix[1]=new int[]{
    
    5,N,N,9,N,N,3};
        matrix[2]=new int[]{
    
    7,N,N,N,8,N,N};
        matrix[3]=new int[]{
    
    N,9,N,N,N,4,N};
        matrix[4]=new int[]{
    
    N,N,8,N,N,5,4};
        matrix[5]=new int[]{
    
    N,N,N,4,5,N,6};
        matrix[6]=new int[]{
    
    2,3,N,N,4,6,N};
        //创建 Graph对象
        Graph graph = new Graph(vertex, matrix);
        //测试, 看看图的邻接矩阵是否ok
        graph.showGraph();
        //测试迪杰斯特拉算法
        graph.dsj(2);//C
        graph.showDijkstra();


    }

}

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

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

    //显示结果
    public void showDijkstra() {
    
    
        vv.show();
    }

    // 显示图
    public void showGraph() {
    
    
        for (int[] link : matrix) {
    
    
            System.out.println(Arrays.toString(link));
        }
    }
    
    public void dsj(int index) {
    
    
        vv = new VisitedVertex(vertex.length, index);
        update(index);//更新index顶点到周围顶点的距离和前驱顶点
        for(int j = 1; j <vertex.length; j++) {
    
    
            index = vv.updateArr();// 选择并返回新的访问顶点
            update(index); // 更新index顶点到周围顶点的距离和前驱顶点
        }
    }



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

// 已访问顶点集合
class VisitedVertex {
    
    
    // 记录各个顶点是否访问过 1表示访问过,0未访问,会动态更新
    public int[] already_arr;
    // 每个下标对应的值为前一个顶点下标, 会动态更新
    public int[] pre_visited;
    // 记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
    public int[] dis;
    
    public VisitedVertex(int length, int index) {
    
    
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];
        //初始化 dis数组
        Arrays.fill(dis, 65535);
        this.already_arr[index] = 1; //设置出发顶点被访问过
        this.dis[index] = 0;//设置出发顶点的访问距离为0

    }
    
    public boolean in(int index) {
    
    
        return already_arr[index] == 1;
    }
    
    public void updateDis(int index, int len) {
    
    
        dis[index] = len;
    }
   
    public void updatePre(int pre, int index) {
    
    
        pre_visited[pre] = 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;
            }
        }
        //更新 index 顶点被访问过
        already_arr[index] = 1;
        return index;
    }

    //显示最后的结果
    //即将三个数组的情况输出
    public void show() {
    
    

        System.out.println("==========================");
        //输出already_arr
        for(int i : already_arr) {
    
    
            System.out.print(i + " ");
        }
        System.out.println();
        //输出pre_visited
        for(int i : pre_visited) {
    
    
            System.out.print(i + " ");
        }
        System.out.println();
        //输出dis
        for(int i : dis) {
    
    
            System.out.print(i + " ");
        }
        System.out.println();
        //为了好看最后的最短距离,我们处理
        char[] vertex = {
    
     'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        int count = 0;
        for (int i : dis) {
    
    
            if (i != 65535) {
    
    
                System.out.print(vertex[count] + "("+i+") ");
            } else {
    
    
                System.out.println("N ");
            }
            count++;
        }
        System.out.println();

    }
}

  执行的结果如下:

二、Floyd算法

  Floyd算法和Dijstra算法一样,也是一种==用于寻找给定的加权图中顶点间最短路径的算法。Floyd算法计算图中各个顶点之间的最短路径。Dijstra算法用于计算图中某一个顶点到其它顶点的最短路径。Floyd算法VSDijstra算法:Dijstra算法通过选定的被访问顶点,求出从出发访问顶点到其它顶点的最短路径;Floyd算法中每一个顶点都是从出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其它顶点的最短路径。
  假设顶点vi到顶点vk的最短路径为Lik,顶点vk到vj的最短路径为Lkj,顶点vi到vj的路径为Lij,顶点vi到vj的最短路径为:min((Lik+Lkj),Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径。
  至于vi到vk的最短路径为Lik或者vk到vj的最短路径Lkj,是以同样的方式获得
  我们以求最短路径的方式加以说明:


  Floyd算法的步骤如下:

  • 1、在第一轮循环中,以A(下标:为0)作为中间顶点【即把A作为中间顶点的所有情况都进行遍历,就会得到更新距离表和前驱的关系】,距离表和前驱关系更新为:

      我们以A顶点作为中间顶点是:B->A->C的距离由N->9,同理C到B:C->A->G的距离由N->12,同理G到C;在更换中间顶点时,循环执行操作,直到所有顶点都作为中间顶点更新后计算结果。

  我们通过用Floyd算法来求解最短路径的问题,具体实现如下:

import java.util.Arrays;

public class FloydAlgorithm {
    
    

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

        //创建 Graph 对象
        Graph graph = new Graph(vertex.length, matrix, vertex);
        //调用弗洛伊德算法
        graph.floyd();
        graph.show();
    }

}

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

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

    // 显示pre数组和dis数组
    public void show() {
    
    

        //为了显示便于阅读,我们优化一下输出
        char[] vertex = {
    
     'A', 'B', 'C', 'D', 'E', 'F', 'G' };
        for (int k = 0; k < dis.length; k++) {
    
    
            // 先将pre数组输出的一行
            for (int i = 0; i < dis.length; i++) {
    
    
                System.out.print(vertex[pre[k][i]] + " ");
            }
            System.out.println();
            // 输出dis数组的一行数据
            for (int i = 0; i < dis.length; i++) {
    
    
                System.out.print("("+vertex[k]+"到"+vertex[i]+"的最短路径是" + dis[k][i] + ") ");
            }
            System.out.println();
            System.out.println();

        }

    }

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

  具体执行代码结果如下:

三、马踏棋盘算法

  提到马踏棋盘算法,看过我前面文章的读者一定很熟悉,在介绍算法的时候就提到这个算法,我们也称之为八皇后问题。本文就不给大家介绍游戏的演示了,感兴趣的读者可以参考这篇文章,里面详细介绍了马踏棋盘的相关问题。本文主要介绍马踏棋盘的实现问题。
  其实,马踏棋盘问题用到的是图的深度优先遍历的应用。如果我们使用回溯法来解决,具体的步骤如下:

  具体思路如下:

  • 1、 创建棋盘 chessBoard , 是一个二维数组
  • 2.、将当前位置设置为已经访问,然后根据当前位置,计算马儿还能走哪些位置,并放入到一个集合中(ArrayList), 最多有8个位置, 每走一步,就使用step+1
  • 3、 遍历ArrayList中存放的所有位置,看看哪个可以走通 , 如果走通,就继续,走不通,就回溯.
  • 4、判断马儿是否完成了任务,使用 step 和应该走的步数比较 , 如果没有达到数量,则表示没有完成任务,将整个棋盘置0

  不过需要我们注意的是:马儿不同的走法(策略),会得到不同的结果,效率也会有影响(优化)。不过,我们可以通过前面提到的贪心算法进行优化,具体如下:

  • 1、 我们获取当前位置,可以走的下一个位置的集合 //获取当前位置可以走的下一个位置的集合 ArrayList<Point> ps = next(new Point(column, row));
  • 2、我们需要对 ps 中所有的Point的下一步的所有集合的数目,进行非递减排序,就好了 , 9, 7, 6, 5, 3, 2 , 1 //递减排序 1, 2, 3,4,5,6, 10, //递增排序
    1, 2, 2, 2, 3,3, 4, 5, 6 // 非递减 9, 7, 6,6, 6, 5,5, 3, 2 ,1 //非递增

  最后我们通过代码将其实现:

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

public class HorseChessboard {
    
    

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

    public static void main(String[] args) {
    
    
        System.out.println("骑士周游算法,开始运行~~");
        //测试骑士周游算法是否正确
        X = 8;
        Y = 8;
        int row = 1; //马儿初始位置的行,从1开始编号
        int column = 1; //马儿初始位置的列,从1开始编号
        //创建棋盘
        int[][] chessboard = new int[X][Y];
        visited = new boolean[X * Y];//初始值都是false
        //测试一下耗时
        long start = System.currentTimeMillis();
        traversalChessboard(chessboard, row - 1, column - 1, 1);
        long end = System.currentTimeMillis();
        System.out.println("共耗时: " + (end - start) + " 毫秒");

        //输出棋盘的最后情况
        for(int[] rows : chessboard) {
    
    
            for(int step: rows) {
    
    
                System.out.print(step + "\t");
            }
            System.out.println();
        }
    }
    
    public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
    
    
        chessboard[row][column] = step;
        //row = 4 X = 8 column = 4 = 4 * 8 + 4 = 36
        visited[row * X + column] = true; //标记该位置已经访问
        //获取当前位置可以走的下一个位置的集合
        ArrayList<Point> ps = next(new Point(column, row));
        //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
        sort(ps);
        //遍历 ps
        while(!ps.isEmpty()) {
    
    
            Point p = ps.remove(0);//取出下一个可以走的位置
            //判断该点是否已经访问过
            if(!visited[p.y * X + p.x]) {
    
    //说明还没有访问过
                traversalChessboard(chessboard, p.y, p.x, step + 1);
            }
        }
        if(step < X * Y && !finished ) {
    
    
            chessboard[row][column] = 0;
            visited[row * X + column] = false;
        } else {
    
    
            finished = true;
        }

    }
    public static ArrayList<Point> next(Point curPoint) {
    
    
        //创建一个ArrayList
        ArrayList<Point> ps = new ArrayList<Point>();
        //创建一个Point
        Point p1 = new Point();
        //表示马儿可以走5这个位置
        if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走6这个位置
        if((p1.x = curPoint.x - 1) >=0 && (p1.y=curPoint.y-2)>=0) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走7这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走0这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走1这个位置
        if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走2这个位置
        if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走3这个位置
        if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
    
    
            ps.add(new Point(p1));
        }
        //判断马儿可以走4这个位置
        if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
    
    
            ps.add(new Point(p1));
        }
        return ps;
    }

    //根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
    public static void sort(ArrayList<Point> ps) {
    
    
        ps.sort(new Comparator<Point>() {
    
    

            @Override
            public int compare(Point o1, Point o2) {
    
    
                // TODO Auto-generated method stub
                //获取到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;
                }
            }

        });
    }
}

  代码执行结果如下:

总结

  我们上篇文章给大家介绍了KMP算法、贪心算法、prim算法和kruskal算法,详细给大家介绍了各大算法常用的场景以及算法的思路和算法在具体应用中应该如何去用代码实现。本文给大家介绍Dijkstra算法、Folyd算法以及最后的马踏棋盘算法。首先给大家介绍Dijkstra算法的相关内容。其实数据结构与算法是特别重要的,在编程中有至关重要的地位,因此,需要我们特别的掌握。生命不息,奋斗不止,我们每天努力,好好学习,不断提高自己的能力,相信自己一定会学有所获。加油!!!

猜你喜欢

转载自blog.csdn.net/Oliverfly1/article/details/114022347