掃描下方二維碼或者微信搜索公衆號
菜鳥飛呀飛
,便可關注微信公衆號,閱讀更多Spring源碼分析
、Java併發編程
、Netty源碼系列
和MySQL工做原理
文章。web
假期過長,致使停更了好長時間,複習一道算法題找找感受。算法
前段時間看到一篇文章,裏面提到了統治世界的十大算法,其中之一就是迪傑斯特拉算法(Dijkstra),該算法主要解決的」最短路徑「這一類問題。說法雖然誇張了點,但它在實際生活中確實應用普遍,例如地圖軟件等,大部分遊戲中自動尋路等功能,使用到的 A*搜索算法也是基於迪傑斯特拉算法優化而來。那麼迪傑斯特拉算法是如何實現的呢?編程
假如咱們如今有以下一個有向圖,圖中有 6 個頂點,編號分別爲 1~6,帶有箭頭的直線表示的是能從一個頂點到達另一個頂點,直線上的數字表示的是兩個頂點之間的距離,如今求頂點 1 到頂點 6 的最短距離。數組
因爲圖中的點比較少,咱們直接手動計算就能算出來這個結果,可是若是頂點不少,有成千上萬個,手動計算就很難了,咱們只能經過程序來計算,那麼咱們的程序該如何寫呢?微信
從圖中咱們能夠看到,頂點 1 只能直接到達頂點 二、三、5,不能直接到達頂點 6,因此要想從 1 到達 6,就必須得從其餘頂點中轉。併發
咱們定義一個數組,用來表示每一個頂點到頂點 1 的距離,數組的索引表示的是頂點編號,數組元素的值表示的是到頂點 1 的最小距離。編輯器
開始咱們在頂點 1 上,頂點 1 能到達 二、三、5,數組的狀態以下。函數
索引 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
最小距離 | 0 | 60 | 10 | null | 50 | null |
從頂點 2 處中轉,頂點 2 能到達頂點 4,距離爲 35,因此頂點 1 到頂點 4 的距離爲 60+35=95,數組狀態以下:源碼分析
索引 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
最小距離 | 0 | 60 | 10 | 95 | 50 | null |
索引 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
最小距離 | 0 | 60 | 10 | 40 | 35 | null |
索引 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
最小距離 | 0 | 60 | 10 | 40 | 35 | 140 |
索引 | 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)的思路比較像,每次找到本身能到達的頂點,而後依次往外擴散,直到遍歷完全部頂點。