圖 —— 最短路徑(一)Dijkstra算法

1、最短路徑概念

最短路徑就是圖中兩點之間經過的最短距離(就是最小權值),圖必須是帶有權值的,可以是無向可以是有向的,

算法具體的形式包括:

  • 確定起點的最短路徑問題:即已知起始結點,求最短路徑的問題。適合使用Dijkstra算法。
  • 確定終點的最短路徑問題:與確定起點的問題相反,該問題是已知終結結點,求最短路徑的問題。在無向圖中該問題與確定起點的問題完全等同,在有向圖中該問題等同於把所有路徑方向反轉的確定起點的問題。
  • 確定起點終點的最短路徑問題:即已知起點和終點,求兩結點之間的最短路徑。
  • 全局最短路徑問題:求圖中所有的最短路徑。適合使用Floyd-Warshall算法。

主要介紹以下幾種算法:

  • Dijkstra最短路算法(單源最短路);
  • Bellman–Ford算法(解決負權邊問題);
  • Floyd最短路算法(全局/多源最短路);

這一節主要說Dijkstra算法;其他兩種算法地址→圖 —— 最短路徑(二)

2、Dijkstra最短路算法圖解

迪科斯徹算法使用了廣度優先搜索解決賦權有向圖或者無向圖的單源最短路徑問題,算法最終得到一個最短路徑樹。該算法常用於路由算法或者作爲其他圖算法的一個子模塊。

dijkstra算法本質上算是貪心的思想,每次在剩餘節點中找到離起點最近的節點放到隊列中,並用來更新剩下的節點的距離,再將它標記上表示已經找到到它的最短路徑,以後不用更新它了。這樣做的原因是到一個節點的最短路徑必然會經過比它離起點更近的節點,而如果一個節點的當前距離值比任何剩餘節點都小,那麼當前的距離值一定是最小的。(剩餘節點的距離值只能用當前剩餘節點來更新,因爲求出了最短路的節點之前已經更新過了)

dijkstra就是這樣不斷從剩餘節點中拿出一個可以確定最短路徑的節點最終求得從起點到每個節點的最短距離。

求解過程,示意圖:
在這裏插入圖片描述

上圖中A→E的最短路徑是:
在這裏插入圖片描述
先定義一個節點類,用來存放圖節點:

/**
 * @ClassName: Node
 * @Author: lzq
 * @Date: 2019/08/23 07:33
 * @Description: 圖節點
 */
public class Node<T> {
    public T name;   //記錄當前節點的名字
    public boolean sign;  //遍歷標誌

    public Node(T name) {
        this.name = name;
        this.sign = false;
    }
}

再定義一個類用來存放上一個節點到當前節點的距離以及上一個節點的位置,以便於最後打印路徑:

/**
 * @ClassName: Distance
 * @Author: lzq
 * @Date: 2019/08/23 07:37
 * @Description: 記錄從上一個節點到當前節點的距離
 */
public class Distance {
    public int par;   //上一個節點的下標  用數組儲存節點的,記錄下標即可
    public int distance;  //從上一個節點到當前節點的距離

    public Distance(int par,int distance) {
        this.par = par;
        this.distance = distance;
    }
}

接着是一個表示圖的類,先定義以下屬性,以及構造函數:

/**
 * @ClassName: Graph
 * @Author: lzq
 * @Date: 2019/08/23 07:41
 * @Description: 圖
 */
public class Graph<T> {
    private int maxNodeNumber = 10;  //每個節點的最大指針數量,也指圖的最多節點數量,可以根據需要調整
    private int INF = 65535;  //表示兩個節點之間不相通,它們的距離就是INF
    private Node<T>[] nodes;    //節點數組,每個圖節點放在這個數組裏面
    private int[][] disIJ;   //表示兩點之間的距離 二維數組的橫縱座標表示對應的起點、終點節點,值表示距離
    private Distance[] sPath;    //用來記錄以當前起點到各個節點的距離,不停的更新它用來獲取最短路徑,這個就是用來儲存最短路徑的
    private int count;   //總的節點數量
    private int n;       //遍歷過的節點數量
    private int start;   //最初的起點
    private int isStart;  //當前起點下標
    private int startToCurDis;     //從開始節點到當前節點的距離,最短路徑的核心算法會用

    //初始化相關變量
    public Graph() {
        this.nodes = new Node[maxNodeNumber];   //假設最多十個節點
        this.disIJ = new int[maxNodeNumber][maxNodeNumber];
        this.sPath = new Distance[maxNodeNumber];
        this.count = 0;
        this.n = 0;

        //初始化各節點之間的距離
        for (int i = 0; i < maxNodeNumber; i++) {
            for (int j = 0; j < maxNodeNumber; j++) {
                this.disIJ[i][j] = INF;
            }
        }
    }

在構造函數裏面創建數組、以及初始化數組之後:

nodes數組:
在這裏插入圖片描述
disIJ數組:
在這裏插入圖片描述
sPath數組:
在這裏插入圖片描述
添加節點、賦權值:

    /**
     * 添加節點名字
     * @param name
     */
    public void addNodeName(T name) {
        this.nodes[n++] = new Node<>(name);
    }

    /**
     * 給節點賦權值
     * @param start
     * @param end
     * @param weight
     */
    public void addNodeWeight(int start,int end,int weight) {
        this.disIJ[start][end] = weight;
    }

當我們調用上述兩個方法將上面示意圖中的節點和權值給進去之後:

nodes數組:
在這裏插入圖片描述
disIJ數組(我把下標對應的元素放在旁邊了,還有用到的節點拿特殊顏色標出來了,便於觀察):

在這裏插入圖片描述
sPath數組,還沒有給他放元素,還是這個樣子:
在這裏插入圖片描述

Dijkstra算法代碼:

    /**
     * 沒有指定起點的話,默認從0開始,一般都是這個
     */
    public void dijkstra() {
        dijkstra1(0);
    }


    /**
     * 指定起點
     * @param startNodeIndex
     */
    public void dijkstra1(int startNodeIndex) {
        if(startNodeIndex >= n || startNodeIndex < 0) {
            return;
        }
        start = startNodeIndex;
        isStart = startNodeIndex;  //起點
        nodes[isStart].sign = true;  //更改遍歷標記,標記被遍歷過了
        count++;    //遍歷過的節點數+1

        //初始化sPath數組
        for (int i = 0; i < n; i++) {
            int dis = disIJ[isStart][i];   //獲取起點到當前節點的距離
            sPath[i] = new Distance(isStart,dis);  //賦值給sPath
        }

        //開始尋找最短路徑,直到每個節點都標記過了
        while (count < n) {
            int startToCurMin = getstartToCurMin();  //找到距離當前起點最近的鄰接節點位置
            int dis = sPath[startToCurMin].distance;  //通過位置拿到距離當前起點以及最近節點之間的距離

            if(dis == INF) {
                //兩點不通,直接跳出,意思就是沒有以當前起點最近的節點,
                break;
            }else {
                isStart = startToCurMin;  //更新起點
                startToCurDis = sPath[isStart].distance; //從最初的起點到新的起點之間的距離
            }
            nodes[isStart].sign = true;  //更新標記
            count++;
            adjustMinPath();  //核心算法,更新最短路徑
        }
        clearSign();
    }

    /**
     * 清除標記
     */
    private void clearSign() {
        count = 0;
        for (int i = 0; i < n; i++) {
            nodes[i].sign = false;
        }
    }

    /**
     * 求最短路徑的核心算法
     */
    private void adjustMinPath() {
        int tempCount = 0;   //表示當前節點
        while (tempCount < n) {
            if(nodes[tempCount].sign || tempCount == isStart) {
                //當前節點找過了或者當前節點是起點自己,直接跳過
                tempCount++;
                continue;
            }
            int curStartToCur = disIJ[isStart][tempCount];  //拿到當前起點到當前節點的距離
            int startToCur = startToCurDis+curStartToCur;  //最初的那個起點到當前節點的距離,最大不超過INF
            int sPathDist = sPath[tempCount].distance;  //獲取已記錄的最初起點到當前節點的距離

            if(startToCur < sPathDist) { //如果已記錄的值大於剛得到的新值,更新
                sPath[tempCount].distance = startToCur;   //更新當前節點到最初起點之間的距離
                sPath[tempCount].par = isStart;  //更新它的父節點
            }
            tempCount++;
        }
    }

    /**
     * 找到距離當前起點最近的鄰接節點,返回該節點的下標
     * @return
     */
    private int getstartToCurMin() {
        int disMin = INF;  //默認距離當前起點的最近的相鄰起點的最短距離
        int index = isStart;  //距離當前起點最近的相鄰節點對應的下標

        //開始查找
        for (int i = 0; i < n; i++) {
            if(i == isStart) {  //自己到自己就不用算了
                continue;
            }
            //這個最近的相鄰節點必須沒有被遍歷,並且距離當前節點的距離是最近的
            if(!nodes[i].sign && sPath[i].distance < disMin) {
                disMin = sPath[i].distance;
                index = i;
            }
        }
        return index;
    }

算法流程解析:

圖先拿下來:
在這裏插入圖片描述
以圖中A爲起點說明,下圖中紅色節點表示已經遍歷過了:

1、當我們根據傳入的數據把sPath數組初始化以後,它就變成了這個樣子,A不能到自己、也不能直接到達C、E所以A與A、B、C、D、E這幾個節點的距離最開始爲INF、50、INF、80、INF:
在這裏插入圖片描述
2、現在找當前起點中距離A最近的節點,也就是50,即B節點,更新這個sPath數組相關的值,得到:
在這裏插入圖片描述
3、繼續找距離A最近的節點,這個時候沒有被遍歷的節點只有C、D、E所以要在C、D、E裏面找,最終找到D,那麼以D爲起點,更新這個sPath數組相關的值;

在這裏插入圖片描述

4、繼續找距離A最近的節點,這個時候要在C、E裏面找,我們找到C,以C爲新的起點,繼續更新sPath:
在這裏插入圖片描述
5、繼續,只剩E了,以E爲起點,更新sPath數組,所有節點遍歷完了,這就找到所有從A出發到達各節點之間的最短路徑了:
在這裏插入圖片描述
可以按需要打印,修改下面兩個方法就好:

  /**
   * 打印最短路徑
   */
  public void show() {
        System.out.println("起點爲"+nodes[start].name+":");
        for (int i = 0; i < n; i++) {
            System.out.print("到達"+nodes[i].name+"距離爲:\t");
            if(sPath[i].distance == INF) {
                System.out.println("INF");
                continue;
            }else {
                System.out.print(sPath[i].distance+"\t");
            }
            dgPrint(i);
            System.out.println();
        }
        System.out.println();
    }

    /**
     * 遞歸打印路徑
     * @param par
     */
    private void dgPrint(int par) {
        if(par == start) {
            System.out.print(nodes[par].name);
            return;
        }
        dgPrint(sPath[par].par);
        System.out.print("-->"+nodes[par].name);
    }

 }   

測試代碼:

   public static void main(String[] args) {
        Graph graph = new Graph();
        graph.addVertex('A');
        graph.addVertex('B');
        graph.addVertex('C');
        graph.addVertex('D');
        graph.addVertex('E');

        graph.addEdge(0,1,50);  //A-->B 50
        graph.addEdge(0,3,80);  //A-->D 80
        graph.addEdge(1,2,60);  //B-->C 60
        graph.addEdge(1,3,90);
        graph.addEdge(2,4,40);
        graph.addEdge(3,2,20);
        graph.addEdge(3,4,70);
        graph.addEdge(4,1,50);

        graph.dijkstra();   //起點爲A
        graph.dijkstra1(2);  //以C爲起點
    }

運行結果:
在這裏插入圖片描述

好了,以上就是最短路徑的狄傑斯特拉算法思想以及一個求最短路徑長度、路徑的的代碼,但是在面試中是不可能有時間讓你去寫這麼多代碼的,一般寫一個方法就夠了,下面就是對應的單個方法求最短路徑;

3、求最短路徑的簡單代碼

(1)如果要求打印出指定起點到其他各點的最短路徑長度

因爲在大部分面試題的的圖相關的編程題都是給的一個關係矩陣,並且節點自己到自己的距離一般爲0、不可達節點之間的距離爲-1,上面例子中爲了便於理解這些關係我都拿INF表示了,那麼如果在面試題中碰到這種編程題,用下面這種寫法就好,數組的下標對應相應的圖節點,傳入的是起始節點下標以及一個表示各個節點之間的權值、方向的數組,返回的是一個表示起始節點到各節點的最短路徑的數組,當然也可以根據題目需要做適當修改,核心代碼就這個:

    /**
     * 求最短路徑的方法 這個方法返回一個數組,裏面是每個節點到起點之間的最短路徑
     * @param startIndex 起始節點
     * @param disIJ 給出的矩陣數組 表示各個節點之間的權值
     * @return  表示起始節點到各個節點的最短路徑長度的數組
     */
    public static int[] dijkstra(int startIndex,int[][] disIJ) {
        boolean[] sign = new boolean[disIJ.length];  //用來表示該位置的元素是否被遍歷,默認全是false
        int[] dis = new int[disIJ.length];  //記錄每個節點到起點之間的距離

        int tempStart = 0;  //臨時起點
        int startToTempStart = 0;  //最初起點到臨時起點之間的距離

        //初始化dis數組
        for (int i = 0; i < disIJ[startIndex].length; i++) {
            dis[i] = disIJ[startIndex][i];
        }

        sign[startIndex] = true;

        int count = 0;  //計數器,記錄有多少個節點被遍歷過了,當所有節點都被遍歷完了就結束了
        while (count < disIJ.length) {
            //每次進來第一步:查找dis數組裏面距離起點最近的那個節點
            int minValue = Integer.MAX_VALUE;
            int index = startIndex;
            for(int i = 0;i < dis.length;i++) {
                if(!sign[i] && dis[i] != -1 && dis[i] < minValue ) {
                    minValue = dis[i];
                    index = i;
                }
            }

            if(minValue == Integer.MAX_VALUE) {  //沒找到,就是沒有,直接退出
                break;
            }else {
                tempStart = index;
                startToTempStart = minValue;
            }

            sign[tempStart] = true;
            count++;

            //更新數組中的值
            for (int i = 0; i < disIJ.length; i++) {
                //如果這個節點遍歷過了或者是當前起點直接跳過
                if(sign[i] || i == tempStart) {
                    continue;
                }
                int tempStartToCur = disIJ[tempStart][i];  //拿到當前起點到當前節點的距離
                if(tempStartToCur == -1) {
                    //這兩點不通,跳過
                    continue;
                }
                int startIndexToCur = startToTempStart+tempStartToCur;  //得到最初起點到當前節點的距離
                int disStartIndexToCur = dis[i];  //獲取原來記錄的這兩個點之間的距離
                if((disStartIndexToCur == -1 && startIndexToCur > 0) || (disStartIndexToCur != -1 && disStartIndexToCur > startIndexToCur)) {
                    dis[i] = startIndexToCur;
                }
            }
        }
        return dis;
    }

我們還是用上面的圖例來測試一下這個代碼:

  public static void main(String[] args) {
       int[][] disIJ = {{0,50,-1,80,-1},
                {-1,0,60,90,-1},
                {-1,-1,0,-1,40},
                {-1,-1,20,0,70},
                {-1,50,-1,-1,0}};

        System.out.println(Arrays.toString(dijkstra(0,disIJ)));
        System.out.println(Arrays.toString(dijkstra(1,disIJ)));
        System.out.println(Arrays.toString(dijkstra(2,disIJ)));
        System.out.println(Arrays.toString(dijkstra(3,disIJ)));
        System.out.println(Arrays.toString(dijkstra(4,disIJ)));
 }

運行結果:
在這裏插入圖片描述

(2)如果要求打印出指定起點到其他各點的最短路徑 即連路徑也要打印出來

我們在上面圖解階段用的是一個Distance類專門記錄距離和是從哪個父節點過來的,打印的時候從這個父節點往上找就是了,所以如果題目要求需要打印路徑的話,我們需要在上面方法添加一個用來記錄路徑的數組:

注意,有 // =========================== 就是如果需要打印路徑的話,需要添加或修改代碼的地方:

/**
     * 求最短路徑的方法 這個方法返回一個數組,裏面是每個節點到起點之間的最短路徑
     * @param startIndex 起始節點
     * @param disIJ 給出的矩陣數組 表示各個節點之間的權值
     * @return 返回一個二維數組,0號下標數組表示起始節點到各個節點的最短路徑長度,1號下標數組
     *         表示起始節點到各個節點的最短路徑 記錄的是父節點下標 我們可以根據它得到最短路徑
     */
    public static int[][] dijkstra(int startIndex,int[][] disIJ) {
        boolean[] sign = new boolean[disIJ.length];  //用來表示該位置的元素是否被遍歷,默認全是false
        int[] dis = new int[disIJ.length];  //記錄每個節點到起點之間的距離
        // 用來記錄路徑,數組下標表示當前節點位置,對應元素值表示父節點位置
        int[] path = new int[disIJ.length];  // ===========================

        int tempStart = 0;  //臨時起點
        int startToTempStart = 0;  //最初起點到臨時起點之間的距離

        //初始化dis數組、初始化path數組
        for (int i = 0; i < disIJ[startIndex].length; i++) {
            dis[i] = disIJ[startIndex][i];
            //初始化每個節點的父節點,把他們都出初始化成起始節點   
            path[i] = startIndex; // ===========================
        }

        sign[startIndex] = true;

        int count = 0;  //計數器,記錄有多少個節點被遍歷過了,當所有節點都被遍歷完了就結束了
        while (count < disIJ.length) {
            //每次進來第一步:查找dis數組裏面距離起點最近的那個節點
            int minValue = Integer.MAX_VALUE;
            int index = startIndex;
            for(int i = 0;i < dis.length;i++) {
                if(!sign[i] && dis[i] != -1 && dis[i] < minValue ) {
                    minValue = dis[i];
                    index = i;
                }
            }

            if(minValue == Integer.MAX_VALUE) {  //沒找到,就是沒有,直接退出
                break;
            }else {
                tempStart = index;
                startToTempStart = minValue;
            }

            sign[tempStart] = true;
            count++;

            //更新數組中的值
            for (int i = 0; i < disIJ.length; i++) {
                //如果這個節點遍歷過了或者是當前起點直接跳過
                if(sign[i] || i == tempStart) {
                    continue;
                }
                int tempStartToCur = disIJ[tempStart][i];  //拿到當前起點到當前節點的距離
                if(tempStartToCur == -1) {
                    //這兩點不通,跳過
                    continue;
                }
                int startIndexToCur = startToTempStart+tempStartToCur;  //得到最初起點到當前節點的距離
                int disStartIndexToCur = dis[i];  //獲取原來記錄的這兩個點之間的距離
                if((disStartIndexToCur == -1 && startIndexToCur > 0) || (disStartIndexToCur != -1 && disStartIndexToCur > startIndexToCur)) {
                    dis[i] = startIndexToCur;
                    //更新這個節點的父節點,能進這個語句,肯定換了父節點
                    path[i] = tempStart;   // ===========================
                }
            }
        }
        return new int[][] {dis,path};  // ===========================
    }

測試代碼:

    public static void main(String[] args) {
        int[][] disIJ = {{0,50,-1,80,-1},
                {-1,0,60,90,-1},
                {-1,-1,0,-1,40},
                {-1,-1,20,0,70},
                {-1,50,-1,-1,0}};

        int[][] disAndPath = dijkstra(0,disIJ);
        System.out.println(Arrays.toString(disAndPath[0]));
        System.out.println(Arrays.toString(disAndPath[1]));

        //打印以指定起點到達每個節點的最短路徑 這裏用數組下標表示的路徑 
        // 如果需要字符表示的話添加一個字符數組打印對應位置的字符就好
        for (int i = 0; i < disAndPath[1].length; i++) {
            dfs(disAndPath[1],0,i);
            System.out.println();
        }
    }

    /**
     * 遞歸打印路徑
     * @param disAndPath
     * @param startIndex
     * @param index
     */
    private static void dfs(int[] disAndPath,int startIndex,int index) {
        if(index == startIndex) {
            System.out.print(index);
            return;
        }
        dfs(disAndPath,startIndex,disAndPath[index]);
        System.out.print("-->"+index);
    }

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章