最短路徑問題——迪傑斯特拉算法(Dijkstra)

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程Netty源碼系列MySQL工做原理文章。web

微信公衆號

假期過長,致使停更了好長時間,複習一道算法題找找感受。算法

前段時間看到一篇文章,裏面提到了統治世界的十大算法,其中之一就是迪傑斯特拉算法(Dijkstra),該算法主要解決的」最短路徑「這一類問題。說法雖然誇張了點,但它在實際生活中確實應用普遍,例如地圖軟件等,大部分遊戲中自動尋路等功能,使用到的 A*搜索算法也是基於迪傑斯特拉算法優化而來。那麼迪傑斯特拉算法是如何實現的呢?編程

假如咱們如今有以下一個有向圖,圖中有 6 個頂點,編號分別爲 1~6,帶有箭頭的直線表示的是能從一個頂點到達另一個頂點,直線上的數字表示的是兩個頂點之間的距離,如今求頂點 1 到頂點 6 的最短距離。數組

示例圖

因爲圖中的點比較少,咱們直接手動計算就能算出來這個結果,可是若是頂點不少,有成千上萬個,手動計算就很難了,咱們只能經過程序來計算,那麼咱們的程序該如何寫呢?微信

思路

從圖中咱們能夠看到,頂點 1 只能直接到達頂點 二、三、5,不能直接到達頂點 6,因此要想從 1 到達 6,就必須得從其餘頂點中轉。併發

咱們定義一個數組,用來表示每一個頂點到頂點 1 的距離,數組的索引表示的是頂點編號,數組元素的值表示的是到頂點 1 的最小距離。編輯器

  1. 開始咱們在頂點 1 上,頂點 1 能到達 二、三、5,數組的狀態以下。函數

    索引 1 2 3 4 5 6
    最小距離 0 60 10 null 50 null
  2. 從頂點 2 處中轉,頂點 2 能到達頂點 4,距離爲 35,因此頂點 1 到頂點 4 的距離爲 60+35=95,數組狀態以下:源碼分析

索引 1 2 3 4 5 6
最小距離 0 60 10 95 50 null
  1. 從頂點 3 處中轉,頂點 3 能到達 4,距離爲 30。若是經過頂點 3 中轉,那麼 1 到達 4 的距離爲 40,小於通過 2 中轉的距離,因此數組中到頂點 4 的距離更新爲 40。頂點 3 還能到達頂點 5,同理,經過頂點 2 中轉,1 到達 5 的距離爲 10+25=35,小於 1 直接到達 5 的距離,所以數組中到達頂點 5 的距離更新爲 35,更新後,數組狀態以下:
索引 1 2 3 4 5 6
最小距離 0 60 10 40 35 null
  1. 從頂點 5 中轉,頂點 5 能到達 2,若是頂點 1 經過頂點 5 到達 2,距離爲 35+30=65,大於頂點 1 直接到達 2,因此不更新。因爲頂點 2 在前面已經遍歷過它能到達哪些點了,因此後面不須要再遍歷它。 頂點 5 還能到達頂點 6,因此此時 1 到達 6 的距離爲 35+105=140,數組狀態以下:
索引 1 2 3 4 5 6
最小距離 0 60 10 40 35 140
  1. 從頂點 4 中轉,頂點 4 也能達到頂點 6。若是經過頂點 4 中轉,那麼 1 到達 6 的距離爲 40+15=55,這個距離小於從 5 中轉,因此更新數組,更新後,數組狀態以下:
索引 1 2 3 4 5 6
最小距離 0 60 10 40 35 55

以上流程,就是迪傑斯特拉算法在計算最短路徑問題時的核心流程。測試

代碼實現

首先咱們須要將這個有向圖用代碼表示出來,一般表示圖的方法有兩種:第一種是鄰接矩陣(也就是二維數組),第二種是鄰接表(也就是數組+鏈表),這裏咱們選用鄰接表法來表示一個有向圖。

另外咱們還須要定義頂點之間的邊,一條邊有起點和終點,還有邊的權重信息,也就是長度,用類 Edge 表示。代碼以下:

private class Edge {
    // 起始定點
    public int s;
    // 終止定點
    public int t;
    // 邊的權重
    public int weight;
 Edge(int s, int t, int weight) { this.s = s; this.t = t; this.weight = weight; } } 複製代碼

有向圖咱們用類 Graph 表示,類中有兩個屬性:頂點個數 v 和描述頂點之間邊的信息的數組 adj,咱們還提供了一個方法:addEdge,用來在兩個頂點之間添加一條邊。代碼以下:

public class Graph {
 // 頂點個數(頂點編號從0開始,在本文例子中,編號爲0的頂點不存在) private int v;  // 記錄每一個頂點的邊 private LinkedList<Edge>[] adj;  public Graph(int v) { this.v = v; // 初始化 this.adj = new LinkedList[v]; for (int i = 0; i < v; i++) { adj[i] = new LinkedList(); } }  // 添加一條邊,從s到達t public void addEdge(int s, int t, int weight) { Edge edge = new Edge(s, t, weight); adj[s].add(edge); } } 複製代碼

定義好了圖的描述信息後,接下來經過代碼來實現迪傑斯特拉算法,其代碼和註釋以下。

有兩處邏輯稍微解釋一下。第一處:flag 數組記錄的是已經遍歷過的頂點,用來防止死循環,例如頂點 1 能到達 2,咱們接着會判斷 2 能到達哪些點,頂點 1 又能到達 5,5 也能到達 2,若是沒有 flag 數組來記錄頂點 2 咱們已經遍歷過了,那麼咱們就會繼續遍歷 2,這樣會致使死循環。第二處:predecessor 數組記錄的是路徑信息,數組的索引表示的頂點編號,元素的值表示的是哪個頂點到達當前頂點的,例如:predecessor[3]=1 表示的是經過頂點 1 到達的頂點 3。

// 採用迪傑斯特拉算法找出從s到t的最短路徑
public void dijkstra(int s, int t) {
    int[] dist = new int[v];    // 記錄s到每一個頂點的最小距離,數組下標表示頂點編號,值表示最小距離
    boolean[] flag = new boolean[v];    // 記錄遍歷過的頂點,數組下標表示頂點編號,值表示是否遍歷過該頂點
    for (int i = 0; i < v; i++) {
        dist[i] = Integer.MAX_VALUE;    // 初始狀態下,將頂點s到其餘頂點的距離都設置爲無窮大
    }
    int[] predecessor = new int[v];     // 記錄路徑,索引表示頂點編號,值表示到達當前頂點的頂點是哪個
    Queue<Integer> queue = new LinkedList<>();
    queue.add(s);
    dist[s] = 0;    // s->s的路徑爲0
    while (!queue.isEmpty()) {
        Integer vertex = queue.poll();
        if (flag[vertex]) continue; // 已經遍歷過該頂點,就再也不遍歷
        flag[vertex] = true;
        for (int i = 0; i < adj[vertex].size(); i++) {
            Edge edge = adj[vertex].get(i);
            if (dist[vertex] < (dist[edge.t] - edge.weight)) {  // 若是出現了比當前路徑小的方式,就更新爲更小路徑
                dist[edge.t] = dist[vertex] + edge.weight;
                predecessor[edge.t] = vertex;
            }
            queue.add(edge.t);
        }
    }
    // 打印路徑
    System.out.println("最短距離:" + dist[t]);
    System.out.print(s);
    print(s, t, predecessor);
}
複製代碼

print 函數的做用是打印從頂點 s 到達頂點 t 的最短路徑中,須要通過哪些點,具體代碼以下,就是一個遞歸調用,比較簡單:

// 打印路徑
private void print(int s, int t, int[] predecessor) {
    if (t == s) {
        return;
    }
    print(s, predecessor[t], predecessor);
    System.out.print(" -> " + t);
}
複製代碼

測試

根據文中的示例,構建一個圖,進行結果測試,代碼以下:

public static void main(String[] args) {
    // 構建圖
    Graph graph = new Graph(7);
    graph.addEdge(1, 2, 60);
    graph.addEdge(1, 3, 10);
    graph.addEdge(1, 5, 50);
    graph.addEdge(2, 4, 35);
    graph.addEdge(3, 4, 30);
    graph.addEdge(3, 5, 25);
    graph.addEdge(4, 6, 15);
    graph.addEdge(5, 2, 30);
    graph.addEdge(5, 6, 105);
    // 計算最短距離
    graph.dijkstra(1, 6);
}
複製代碼

測試結果:

測試結果

總結

迪傑斯特拉算法的思想與廣度優先搜索(BFS)的思路比較像,每次找到本身能到達的頂點,而後依次往外擴散,直到遍歷完全部頂點。

微信公衆號
相關文章
相關標籤/搜索