本文詳細介紹了圖的最短路徑的概念,然後介紹了求最短路徑的兩種算法:Dijkstra算法和Floyd算法的原理,最後提供了基於鄰接矩陣和鄰接表的圖對兩種算法的Java實現。
閱讀本文需要一定的圖的基礎,如果對於圖不是太明白的可以看看這篇文章:圖的入門概念以及存儲結構、遍歷方式介紹和Java代碼的實現。
1 最短路徑的概述
在生活中,圖形結構的應用是最廣泛的。比如常見的交通路線選擇,站點可以看作頂點,站點之間如果有路徑,則算作兩點之間的邊或者弧,站點之間的通行時間,可以看作邊或者弧的權值。
上圖就是生活中出行路線的選擇映射到圖形結構的案例。頂點作爲站點,站點之間能夠到達則擁有邊,站點的之間的通行時間則是邊的權值。
對於出行路線的選擇,不同的人有不同的選擇。其中有一種很常見的選擇是要求出發地和目的地之間的總通行時間最短,而不在乎中途到底有幾站。畢竟通行時間對於很多人來說是寶貴的!
這樣的問題轉轉換爲數學模型,就是求帶權圖的最短路徑,就是求帶權圖形兩頂點之間的權值最小的路徑。即如果從圖中某一頂點(源點)到達另一頂點(終點)的路徑可能不止一條,如何找到一條路徑使得沿此路徑上各邊的權值總和(稱爲路徑長度)達到最小。
實際上最短路徑有兩重含義,一個兩頂點之間的路徑數量最少,另一個是兩頂點之間的路徑距離最短,本次主要解決路徑距離最短的問題,即最小權值和。常見的解決算法一般是兩種,迪傑斯特拉(Dijkstra)算法和弗洛伊德(Floyd)算法。
2 傑斯特拉(Dijkstra)算法
2.1 原理
迪傑斯特拉(Dijkstra)算法是由荷蘭計算機科學家狄克斯特拉於1959 年提出的,因此又叫狄克斯特拉算法。是從一個頂點到其餘各頂點的最短路徑算法,解決的是尋找給定的加權圖中指定頂點間最短路徑問題的算法
Dijkstra算法並不是一下子就求出了起點到終點的最短路徑,而是採用的是貪心算法策略,一步步求出它們之間頂點的最短路徑,過程中都是基於已經求出的最短路徑的基礎上,求得更遠頂點的最短路徑,最終得到起點和終點的最短路徑。
通用步驟如下:
- 指定兩個集合S和U。S的作用是記錄已求出最短路徑的頂點,而U則是記錄還未求出最短路徑的頂點,以及這些頂點到起始頂點的權。
- 指定一個起始頂點A,存入集合S中,其他頂點以及到頂點A的權存入集合U中,從U中找出並移除路徑最短的頂點B,並將其加入到S中,並且更新U中對應的路徑權值(更新源點將新加入節點作爲中間節點到達其它節點的距離);重複該操作直到遍歷所有頂點,此時S中的集合就是起點A到其他各個頂點的最短路徑。
迪傑斯特拉算法只支持非負權圖,它計算的是單源最短路徑,即單個源點到剩餘節點的最短路徑,時間複雜度爲O(n²),對稀疏圖運行更快。如果想知道所有頂點到所有頂點的最短路徑,那麼等於在原有算法的基礎上,再來一次循環,此時整個算法的時間複雜度就成了O(n3)。
2.2 案例分析
該案例對應着下面實現代碼中的案例,設起始點爲A,初始化A到其他點的路徑:{0, 99, 8, 2, 99, 3, 99},初始化標誌位數組{true, false, false, false, false, false, false}。
開始第一輪循環,排除已找到的路徑,即排除0,尋找到A的最短路徑,找到了A-D,即索引爲3的頂點,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的D可達的頂點路徑到A點的最短路徑,如果經過D點的路徑比數組中已存在的最短路徑小,那麼更新值。
這裏D點可達C、B,D-C+A-D=7<8,因此更新A-C的最短路徑爲7;D-B+A-D=11<99,因此更新A-B的最短路徑爲11,第一輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 99, 3, 99},標誌位數組爲{true, false, false, true, false, false, false}:
開始第二輪循環,排除已找到的路徑,即排除0、2,尋找到A的最短路徑,這裏找到3,即索引爲5的頂點,即A-F,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的F可達的頂點路徑到A點的最短路徑,如果經過F點的路徑比數組中已存在的最短路徑小,那麼更新值。
這裏F點可達G,F-G+A-F=12<99,因此更新A-G的最短路徑爲12,第二輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 99, 3, 12},標誌位數組爲{true, false, false, true, false, true, false}。
開始第三輪循環,排除已找到的路徑,即排除0、2、3,尋找到A的最短路徑,這裏找到7,即索引爲2的頂點,即A-C,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的C可達的頂點路徑到A點的最短路徑,如果經過C點的路徑比數組中已存在的最短路徑小,那麼更新值。
這裏C點可達B,C-B+A-C=11 = 11,因此不更新A-B的最短路徑,第三輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 99, 3, 12},標誌位數組爲{true, false, true, true, false, true, false}。
開始第四輪循環,排除已找到的路徑,即排除0、2、3、7,尋找到A的最短路徑,這裏找到11,即索引爲1的頂點,即A-B,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的B可達的頂點路徑到A點的最短路徑,如果經過B點的路徑比數組中已存在的最短路徑小,那麼更新值。
這裏B點可達E,B-E+A-B=18 < 99,因此更新A-E的最短路徑爲18,第四輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 18, 3, 12},標誌位數組爲{true, true, true, true, false, true, false}。
開始第五輪循環,排除已找到的路徑,即排除0、2、3、7、11,尋找到A的最短路徑,這裏找到12,即索引爲6的頂點,即A-G,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的G可達的頂點路徑到A點的最短路徑,如果經過G點的路徑比數組中已存在的最短路徑小,那麼更新值。
排除已找到的頂點,這裏G點可達E,G-E+A-G=18 = 18,因此不更新最短路徑,第五輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 18, 3, 12},標誌位數組爲{true, true, true, true, false, true, true}。
開始第六輪循環,排除已找到的路徑,即排除0、2、3、7、11、12,尋找到A的最短路徑,這裏找到18,即索引爲4的頂點,即A-E,設置對應位置的最短路徑標誌位爲true,更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏只需要更新新找到的E可達的頂點路徑到A點的最短路徑,如果經過E點的路徑比數組中已存在的最短路徑小,那麼更新值。
排除已找到的頂點,這裏不更新最短路徑,第四輪循環結束,此時最短路徑數組爲{0, 11, 7, 2, 18, 3, 12},標誌位數組爲{true, true, true, true, true, true, true}。
此時大循環結束,Dijkstra算法結束,頂點A到各個頂點的最短路徑已經找到,即A到{A,B,C,D,E,F,G}的最短路徑爲{0, 11, 7, 2, 18, 3, 12}。
3 弗洛伊德(Floyd)算法
3.1 原理
弗洛伊德(Floyd)算法又稱插點法,是一種利用動態規劃的思想尋找給定的加權圖中多源點之間最短路徑的算法。算出來的結果是所有的節點到其餘各節點之間的最短距離。
通用步驟如下:
- 設圖頂點數爲N。首先需要準備一個長度爲N的距離矩陣S,矩陣S中的元素a[i][j]=sum的表示頂點i到頂點j的最短路徑爲sum;
- 然後對S矩陣進行初始化,距離矩陣S中頂點a[i][j]的值爲頂點i到頂點j的直接權值;
- 然後對S矩陣循環進行N次更新,在第k次更新時,如果S矩陣的a[i][j] > a[i][k]+a[k][j],那麼更新a[i][j]=a[i][k]+a[k][j]。循環更新完畢,則算法完成,所有的節點到其餘各節點之間的最短距離已經找到了。
相比於Dijkstra 算法,Floyd算法支持帶有負權邊的圖,但是不能解決帶有“負權迴路”(或者叫“負權環”)的圖,實際上如果一個圖中帶有“負權迴路”那麼這個圖則沒有最短路徑。
Floyd算法的時間複雜度同樣是時間複雜度O(n3),空間複雜度是O(n2),代碼非常簡單,但是思想相卻是非常的巧妙,將所有的可能都枚舉出來一一對比,取最小值,這樣最終會得到最小值。
3.2 案例分析
該案例對應着下面實現代碼中的案例:
首先初始化距離矩陣S如下:
然後就是三層嵌套循環,開始第一輪大循環,即當k=0,循環遍歷S矩陣,判斷是否小於 shortestPath[i][j],即所有的路徑都經過A點中轉,如果經過A中轉後的路徑shortestPath[i][0] + shortestPath[k][0]< shortestPath[i][j],自然更新路徑:shortestPath[i][j]= shortestPath[i][0] + shortestPath[k][0]。一輪大循環之後的數組如下:
然後經過一共經過N次的大循環,表示經過所有的頂點,最終取得的矩陣如下:
4 鄰接矩陣加權圖實現
這裏的實現能夠構造一個基於鄰接矩陣實現無向加權圖的類,並且提供深度優先遍歷和廣度優先遍歷的方法,提供獲取邊集數組的方法,提供Prim和Kruskal兩種求最小生成樹的方法,提供Dijkstra和Floyd兩種求最短路徑的方法。
/**
* 無向加權圖鄰接矩陣實現
* {@link MatrixDijkstraAndFloyd#MatrixDijkstraAndFloyd(Object[], Edge[])} 構建無向加權圖
* {@link MatrixDijkstraAndFloyd#DFS()} 深度優先遍歷無向加權圖
* {@link MatrixDijkstraAndFloyd#BFS()} 廣度優先遍歷無向加權圖
* {@link MatrixDijkstraAndFloyd#toString()} 輸出無向加權圖
* {@link MatrixDijkstraAndFloyd#prim()} Prim算法實現最小生成樹
* {@link MatrixDijkstraAndFloyd#kruskal()} Kruskal算法實現最小生成樹
* {@link MatrixDijkstraAndFloyd#kruskalAndPrim()} Kruskal算法結合Prim算法實現最小生成樹
* {@link MatrixDijkstraAndFloyd#getEdges()} 獲取邊集數組
* {@link MatrixDijkstraAndFloyd#dijkstra(int)} ()} Dijkstra算法獲取指定頂點到所有頂點的最短路徑
* {@link MatrixDijkstraAndFloyd#dijkstra(int, int)} Dijkstra算法獲取指定頂點到指定頂點的最短路徑
* {@link MatrixDijkstraAndFloyd#floyd()} Floyd獲取所有頂點到所有頂點的最短路徑
*
* @author lx
*/
public class MatrixDijkstraAndFloyd<E> {
/**
* 頂點數組
*/
private Object[] vertexs;
/**
* 鄰接矩陣
*/
private int[][] matrix;
/**
*
*/
private Edge<E>[] edges;
/**
* 由於是加權圖,這裏設置一個邊的權值上限,任何邊的最大權值不能大於等於該值,在實際應用中,該值應該根據實際情況確定
*/
private static final int NO_EDGE = 99;
/**
* 邊對象,具有權值,在構建加權無向圖時使用
*/
private static class Edge<E> {
private E from;
private E to;
private int weight;
public Edge(E from, E to, int weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
@Override
public String toString() {
return "Edge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
/**
* 創建無向加權圖
*
* @param vertexs 頂點數組
* @param edges 邊對象數組
*/
public MatrixDijkstraAndFloyd(Object[] vertexs, Edge<E>[] edges) {
//初始化邊數組
this.edges = edges;
// 初始化頂點數組,並添加頂點
this.vertexs = Arrays.copyOf(vertexs, vertexs.length);
// 初始化邊矩陣,並預先填充邊信息
this.matrix = new int[vertexs.length][vertexs.length];
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
if (i == j) {
this.matrix[i][j] = 0;
} else {
this.matrix[i][j] = NO_EDGE;
}
}
}
for (Edge<E> edge : edges) {
// 讀取一條邊的起始頂點和結束頂點索引值
int p1 = getPosition(edge.from);
int p2 = getPosition(edge.to);
//對稱的兩個點位都置爲edge.weight,無向圖可以看作相互可達的有向圖
this.matrix[p1][p2] = edge.weight;
this.matrix[p2][p1] = edge.weight;
}
}
/**
* 獲取某條邊的某個頂點所在頂點數組的索引位置
*
* @param e 頂點的值
* @return 所在頂點數組的索引位置, 或者-1 - 表示不存在
*/
private int getPosition(E e) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i] == e) {
return i;
}
}
return -1;
}
/**
* 深度優先搜索遍歷圖,類似於樹的前序遍歷,
*/
public void DFS() {
//新建頂點訪問標記數組,對應每個索引對應相同索引的頂點數組中的頂點
boolean[] visited = new boolean[vertexs.length];
//初始化所有頂點都沒有被訪問
for (int i = 0; i < vertexs.length; i++) {
visited[i] = false;
}
System.out.println("DFS: ");
System.out.print("\t");
for (int i = 0; i < vertexs.length; i++) {
if (!visited[i]) {
DFS(i, visited);
}
}
System.out.println();
}
/**
* 深度優先搜索遍歷圖的遞歸實現,類似於樹的先序遍歷
* 因此模仿樹的先序遍歷,同樣借用棧結構,這裏使用的是方法的遞歸,隱式的借用棧
*
* @param i 頂點索引
* @param visited 訪問標誌數組
*/
private void DFS(int i, boolean[] visited) {
visited[i] = true;
System.out.print(vertexs[i] + " ");
// 遍歷該頂點的所有鄰接點。若該鄰接點是沒有訪問過,那麼繼續遞歸遍歷領接點
for (int w = firstVertex(i); w >= 0; w = nextVertex(i, w)) {
if (!visited[w]) {
DFS(w, visited);
}
}
}
/**
* 廣度優先搜索圖,類似於樹的層序遍歷
* 因此模仿樹的層序遍歷,同樣借用隊列結構
*/
public void BFS() {
// 輔組隊列
Queue<Integer> indexLinkedList = new LinkedList<>();
//新建頂點訪問標記數組,對應每個索引對應相同索引的頂點數組中的頂點
boolean[] visited = new boolean[vertexs.length];
for (int i = 0; i < vertexs.length; i++) {
visited[i] = false;
}
System.out.println("BFS: ");
System.out.print("\t");
for (int i = 0; i < vertexs.length; i++) {
if (!visited[i]) {
visited[i] = true;
System.out.print(vertexs[i] + " ");
indexLinkedList.add(i);
}
if (!indexLinkedList.isEmpty()) {
//j索引出隊列
Integer j = indexLinkedList.poll();
//繼續訪問j的鄰接點
for (int k = firstVertex(j); k >= 0; k = nextVertex(j, k)) {
if (!visited[k]) {
visited[k] = true;
System.out.print(vertexs[k] + " ");
//繼續入隊列
indexLinkedList.add(k);
}
}
}
}
System.out.println();
}
/**
* 返回頂點v的第一個鄰接頂點的索引,失敗則返回-1
*
* @param v 頂點v在數組中的索引
* @return 返回頂點v的第一個鄰接頂點的索引,失敗則返回-1
*/
private int firstVertex(int v) {
//如果索引超出範圍,則返回-1
if (v < 0 || v > (vertexs.length - 1)) {
return -1;
}
/*根據鄰接矩陣的規律:頂點索引v對應着邊二維矩陣的matrix[v][i]一行記錄
* 從i=0開始*/
for (int i = 0; i < vertexs.length; i++) {
if (matrix[v][i] != 0 && matrix[v][i] != NO_EDGE) {
return i;
}
}
return -1;
}
/**
* 返回頂點v相對於w的下一個鄰接頂點的索引,失敗則返回-1
*
* @param v 頂點索引
* @param w 第一個鄰接點索引
* @return 返回頂點v相對於w的下一個鄰接頂點的索引,失敗則返回-1
*/
private int nextVertex(int v, int w) {
//如果索引超出範圍,則返回-1
if (v < 0 || v > (vertexs.length - 1) || w < 0 || w > (vertexs.length - 1)) {
return -1;
}
/*根據鄰接矩陣的規律:頂點索引v對應着邊二維矩陣的matrix[v][i]一行記錄
* 由於鄰接點w的索引已經獲取了,所以從i=w+1開始尋找*/
for (int i = w + 1; i < vertexs.length; i++) {
if (matrix[v][i] != 0 && matrix[v][i] != NO_EDGE) {
return i;
}
}
return -1;
}
/**
* 輸出圖
*
* @return 輸出圖字符串
*/
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
stringBuilder.append(matrix[i][j]).append("\t");
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
/**
* Prim算法求最小生成樹
*/
public void prim() {
System.out.println("prim: ");
//對應節點應該被連接的前驅節點,用來輸出
//默認爲0,即前驅結點爲第一個節點
int[] mid = new int[matrix.length];
//如果某頂點作爲末端頂點被連接,對應位置應該爲true
//第一個頂點默認被連接
boolean[] connected = new boolean[matrix.length];
connected[0] = true;
//存儲未連接頂點到已連接頂點的最短距離(最小權)
int[] dis = new int[matrix.length];
//首先將矩陣第一行即其他頂點到0索引頂點的權值拷貝進去
System.arraycopy(matrix[0], 0, dis, 0, matrix.length);
//存儲路徑長度
int sum = 0;
//最小權值
int min;
/*默認第一個頂點已經找到了,因此最多還要需要大循環n-1次*/
for (int k = 1; k < matrix.length; k++) {
min = NO_EDGE;
//最小權值的頂點的索引
int minIndex = 0;
/*尋找權值最小的且未被連接的頂點索引*/
for (int i = 1; i < matrix.length; i++) {
//排除已連接的頂點,排除權值等於0的值,這裏權值等於0表示已生成的最小生成樹的頂點都未能與該頂點連接
if (!connected[i] && dis[i] != 0 && dis[i] < min) {
min = dis[i];
minIndex = i;
}
}
//如果沒找到,那麼該圖可能不是連通圖,直接返回了,此時最小生成樹沒啥意義
if (minIndex == 0) {
return;
}
//權值和增加
sum += min;
//該新連接頂點對應的索引值變成true,表示已被連接,後續判斷時跳過該頂點
connected[minIndex] = true;
//輸出對應的前驅頂點到該最小頂點的權值
System.out.println("\t" + vertexs[mid[minIndex]] + " ---> " + vertexs[minIndex] + " 權值:" + min);
/*在新頂點minIndex加入之前的其他所有頂點到連接頂點最小的權值已經計算過了
因此只需要更新其他未連接頂點到新連接頂點minIndex是否還有更短的權值,有的話就更新找到距離已連接的頂點權最小的頂點*/
for (int i = 1; i < matrix.length; i++) {
//如果該頂點未連接
if (!connected[i]) {
/*如果新頂點到未連接頂點i的權值不爲0,並且比原始頂點到未連接頂點i的權值還要小,那麼更新對應位置的最小權值*/
if (matrix[minIndex][i] != 0 && dis[i] > matrix[minIndex][i]) {
//更新最小權值
dis[i] = matrix[minIndex][i];
//更新前驅節點索引爲新加入節點索引
mid[i] = minIndex;
}
}
}
}
System.out.println("\t" + "sum: " + sum);
}
/**
* Kruskal算法求最小生成樹傳統實現,要求知道邊集數組,和頂點數組
*/
public void kruskal() {
System.out.println("Kruskal: ");
//由於創建圖的時候保存了邊集數組,這裏直接使用就行了
//Edge[] edges = getEdges();
//this.edges=edges;
//對邊集數組進行排序
Arrays.sort(this.edges, Comparator.comparingInt(o -> o.weight));
// 用於保存已有最小生成樹中每個頂點在該最小樹中的最終終點的索引
int[] vends = new int[this.edges.length];
//能夠知道終點索引範圍是[0,this.edges.length-1],因此填充edges.length表示沒有終點
Arrays.fill(vends, this.edges.length);
int sum = 0;
for (Edge<E> edge : this.edges) {
// 獲取第i條邊的起點索引from
int from = getPosition(edge.from);
// 獲取第i條邊的終點索引to
int to = getPosition(edge.to);
// 獲取頂點from在"已有的最小生成樹"中的終點
int m = getEndIndex(vends, from);
// 獲取頂點to在"已有的最小生成樹"中的終點
int n = getEndIndex(vends, to);
// 如果m!=n,意味着沒有形成環路,則可以添加,否則直接跳過,進行下一條邊的判斷
if (m != n) {
//添加設置原始終點索引m在已有的最小生成樹中的終點爲n
vends[m] = n;
System.out.println("\t" + vertexs[from] + " ---> " + vertexs[to] + " 權值:" + edge.weight);
sum += edge.weight;
}
}
System.out.println("\t" + "sum: " + sum);
//System.out.println(Arrays.toString(this.edges));
}
/**
* 獲取頂點索引i的終點如果沒有終點則返回頂點索引本身
*
* @param vends 頂點在最小生成樹中的終點
* @param i 頂點索引
* @return 頂點索引i的終點如果沒有終點則返回頂點索引本身
*/
private int getEndIndex(int[] vends, int i) {
//這裏使用循環查找的邏輯,尋找的是最終的終點
while (vends[i] != this.edges.length) {
i = vends[i];
}
return i;
}
/**
* 如果沒有現成的邊集數組,那麼根據鄰接矩陣結構獲取圖中的邊集數組
*
* @return 圖的邊集數組
*/
private Edge[] getEdges() {
List<Edge> edges = new ArrayList<>();
/*遍歷矩陣數組 只需要遍歷一半就行了*/
for (int i = 0; i < vertexs.length; i++) {
for (int j = i + 1; j < vertexs.length; j++) {
//如果存在邊
if (matrix[i][j] != NO_EDGE && matrix[i][j] != 0) {
edges.add(new Edge<>(vertexs[i], vertexs[j], matrix[i][j]));
//edges[index++] = new Edge(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges.toArray(new Edge[0]);
}
/**
* Kruskal結合Prim算法.不需要知道邊集,只需要矩陣數組,和頂點數組
* 同樣是求最小權值的邊,但是有一個默認起點頂點,該起點可以是要求[0,頂點數量-1]之間的任意值,同時查找最小權的邊。
* 可能會有Bug,目前未發現
*/
public void kruskalAndPrim() {
System.out.println("kruskalAndPrim: ");
//已經找到的邊攜帶的頂點對應的索引將變爲true,其餘未找到邊對應的頂點將是false
boolean[] connected = new boolean[matrix.length];
//這裏選擇第一個頂點爲起點,表示以該頂點開始尋找包含該頂點的最小邊
connected[0] = true;
int sum = 0, n1 = 0, n2 = 0;
//最小權值
int min;
while (true) {
min = NO_EDGE;
/*找出所有帶有已找到頂點的邊中,最小權值的邊,只需要尋找對稱矩陣的一半即可*/
//第一維
for (int i = 0; i < matrix.length; i++) {
//第二維
for (int j = i + 1; j < matrix.length; j++) {
//排除等於0的,排除兩個頂點都找到了的,這裏實際上已經隱含了排除環的邏輯,如果某條邊的兩個頂點都找到了,那麼如果算上該條邊,肯定會形成環
//尋找剩下的最小的權值的邊
if (matrix[i][j] != 0 && connected[i] != connected[j] && matrix[i][j] < min) {
min = matrix[i][j];
n1 = i;
n2 = j;
}
}
}
//如果沒找到最小權值,該圖可能不是連通圖,或者已經尋找完畢,直接返回
if (min == NO_EDGE) {
System.out.println("\t" + "sum:" + sum);
return;
}
//已經找到的邊對應的兩個頂點都置爲true
connected[n1] = true;
connected[n2] = true;
//輸出找到的邊和最小權值
System.out.println("\t" + vertexs[n1] + " ---> " + vertexs[n2] + " 權值:" + min);
sum += min;
}
}
/**
* Dijkstra算法求最短路徑。
*
* @param start 起始頂點索引。即計算"頂點vs"到其它頂點的最短路徑。
*/
public void dijkstra(int start) {
checkIndex(start);
int[] shortestPathance = getShortestDistance(start, vertexs.length);
// 打印Dijkstra最短路徑的結果
System.out.println("Dijkstra(" + vertexs[start] + "):");
for (int i = 0; i < vertexs.length; i++) {
System.out.println("\t(" + vertexs[start] + " ---> " + vertexs[i] + ")最短路徑:" + shortestPathance[i]);
}
}
/**
* Dijkstra算法求最短路徑
*
* @param start 起始點
* @param end 終點,如果end=vertexs.length說明是遍歷查找所有的最短路徑
* @return 起始頂點到其他點或者指定點的最短權值
*/
private int[] getShortestDistance(int start, int end) {
/*1、該數組存放起始頂點到其他點的權值*/
int[] shortestPathance = new int[vertexs.length];
//初始化數據
//首先設置起始點到頂點i到的最短路徑爲起始點到頂點i的權。
System.arraycopy(matrix[start], 0, shortestPathance, 0, matrix.length);
/*2、標誌位數組.某個位置如果爲true表示對應位置的頂點到起始頂點的最短路徑已成功獲取。*/
boolean[] shortest = new boolean[vertexs.length];
//首先設置起始點到自己的的路徑已經找到了,爲0
shortest[start] = true;
/*3、最多遍歷vertexs.length-1次;每次找出起始點到一個頂點的最短路徑。*/
int k;
int min;
for (int i = 1; i < vertexs.length; i++) {
k = 0;
// 尋找當前最小的路徑;
min = NO_EDGE;
for (int j = 0; j < vertexs.length; j++) {
//排除已經找到的最短路徑之後,找到離start最近的頂點(k)。
if (!shortest[j] && shortestPathance[j] < min) {
min = shortestPathance[j];
k = j;
}
}
//先設置起始點到新頂點k的最短路徑已經找到
shortest[k] = true;
if (end != vertexs.length && k == end) {
break;
}
//更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏指需要更新新加入的已找到的可達頂點的路徑.
for (int j = 0; j < vertexs.length; j++) {
int tmp = matrix[k][j];
//排除已經找到的最短路徑,排除未連接的路徑,排除等於0的路徑(連接自己)之後
//找到離start最如果新的最短路徑比以前的最短路徑還要短,則更新最短路徑。
if (!shortest[j] && tmp != NO_EDGE && tmp != 0 && ((tmp = min + tmp) < shortestPathance[j])) {
shortestPathance[j] = tmp;
}
}
}
return shortestPathance;
}
/**
* 索引檢查
*
* @param index 多個索引
*/
private void checkIndex(int... index) {
for (int i : index) {
if (i < 0 || i >= vertexs.length) {
throw new ArrayIndexOutOfBoundsException("索引越界:" + i);
}
}
}
/**
* Dijkstra算法求最短路徑。
*
* @param start 起始頂點索引。
* @param end 結束點索引
*/
public void dijkstra(int start, int end) {
checkIndex(start, end);
int[] shortestPathance = getShortestDistance(start, end);
// 打印Dijkstra最短路徑的結果
System.out.println("Dijkstra(" + vertexs[start] + " ---> " + vertexs[end] + ")最短路徑:" + shortestPathance[end]);
}
/**
* Floyd算法獲取所有頂點到所有頂點的最短路徑,代碼很簡單,思想很巧妙
*/
public void floyd() {
//路徑矩陣(兩頂點最短路徑,即最小權值)
int[][] shortestPath = new int[matrix.length][matrix.length];
/*初始化數據*/
for (int i = 0; i < matrix.length; i++) {
System.arraycopy(matrix[i], 0, shortestPath[i], 0, vertexs.length);
}
// 計算最短路徑
for (int k = 0; k < matrix.length; k++) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
//要求經過下標k頂點的兩個路徑都不能等於NO_EDGE,否則就是沒有路徑,NO_EDGE應該選取的足夠的大,否則可能出錯
int tmp = (shortestPath[i][k] == NO_EDGE || shortestPath[k][j] == NO_EDGE) ? NO_EDGE : (shortestPath[i][k] + shortestPath[k][j]);
// 如果經過下標爲k頂點路徑比原兩點間路徑更短,則更新shortestPath[i][j]
if (shortestPath[i][j] > tmp) {
// i到j最短路徑對應的值設爲經過k的更小的一個
shortestPath[i][j] = tmp;
}
}
}
}
/*輸出路徑矩陣*/
System.out.println("Floyd: ");
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
System.out.print("\t" + shortestPath[i][j]);
}
System.out.println();
}
}
public static void main(String[] args) {
//頂點數組
Character[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//邊數組,加權值
Edge[] edges = {
new Edge<>('A', 'C', 8),
new Edge<>('D', 'A', 2),
new Edge<>('A', 'F', 3),
new Edge<>('B', 'C', 4),
new Edge<>('C', 'D', 5),
new Edge<>('E', 'G', 6),
new Edge<>('E', 'B', 7),
new Edge<>('D', 'B', 9),
new Edge<>('F', 'G', 9)};
//構建圖
MatrixDijkstraAndFloyd<Character> matrixDijkstraAndFloyd = new MatrixDijkstraAndFloyd<Character>(vexs, edges);
//輸出圖
System.out.println(matrixDijkstraAndFloyd);
//深度優先遍歷
matrixDijkstraAndFloyd.DFS();
//廣度優先遍歷
matrixDijkstraAndFloyd.BFS();
//Prim算法輸出最小生成樹
matrixDijkstraAndFloyd.prim();
//Kruskal算法輸出最小生成樹
matrixDijkstraAndFloyd.kruskal();
//Kruskal算法結合Prim算法輸出最小生成樹,可能會有Bug,目前未發現
matrixDijkstraAndFloyd.kruskalAndPrim();
// Dijkstra算法獲取某個索引的頂點到其它各個頂點的最短距離
// 這裏參數是索引,也可以是一個頂點,需要稍微修改代碼獲取頂點的索引,比較簡單這裏就不做了
matrixDijkstraAndFloyd.dijkstra(0);
// Dijkstra算法獲取一個頂點到另一個頂點的最短距離
matrixDijkstraAndFloyd.dijkstra(2, 0);
// Floyd算法獲取所有頂點到所有頂點的最短路徑
matrixDijkstraAndFloyd.floyd();
}
}
5 鄰接表加權圖實現
這裏的實現能夠構造一個基於鄰接表實現無向加權圖的類;並且提供深度優先遍歷和廣度優先遍歷的方法,提供獲取邊集數組的方法,提供Prim和Kruskal兩種求最小生成樹的方法,,提供Dijkstra和Floyd兩種求最短路徑的方法。
/**
* 無向加權圖鄰接表實現
* {@link ListDijkstraAndFloyd#ListDijkstraAndFloyd(Object[], Edge[])} 構建無向加權圖
* {@link ListDijkstraAndFloyd#DFS()} 深度優先遍歷無向加權圖
* {@link ListDijkstraAndFloyd#BFS()} 廣度優先遍歷無向加權圖
* {@link ListDijkstraAndFloyd#toString()} 輸出無向加權圖
* {@link ListDijkstraAndFloyd#prim()} Prim算法實現最小生成樹
* {@link ListDijkstraAndFloyd#kruskal()} Kruskal算法實現最小生成樹
* {@link ListDijkstraAndFloyd#getEdges()} 獲取邊集數組
* {@link ListDijkstraAndFloyd#dijkstra(int)} ()} 獲取指定頂點到所有頂點的最短路徑
* {@link ListDijkstraAndFloyd#dijkstra(int, int)} 獲取指定頂點到指定頂點的最短路徑
* {@link ListDijkstraAndFloyd#floyd()} Floyd獲取所有頂點到所有頂點的最短路徑
*
* @author lx
*/
public class ListDijkstraAndFloyd<E> {
/**
* 頂點類
*
* @param <E>
*/
private class Node<E> {
/**
* 頂點信息
*/
E data;
/**
* 指向第一條依附該頂點的邊
*/
LNode firstLNode;
public Node(E data, LNode firstLNode) {
this.data = data;
this.firstLNode = firstLNode;
}
}
/**
* 邊表節點類
*/
private class LNode {
/**
* 該邊所指向的頂點的索引位置
*/
int vertex;
/**
* 該邊的權值
*/
int weight;
/**
* 指向下一條邊的指針
*/
LNode nextLNode;
}
/**
* 邊對象,具有權值,在構建加權無向圖時使用
*/
private static class Edge<E> {
private E from;
private E to;
private int weight;
public Edge(E from, E to, int weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
@Override
public String toString() {
return "Edge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
/**
* 頂點數組
*/
private Node<E>[] vertexs;
/**
* 邊數組
*/
private Edge<E>[] edges;
/**
* 由於是加權圖,這裏設置一個邊的權值上限,任何邊的最大權值不能大於等於該值,在實際應用中,該值應該根據實際情況確定
*/
private static final int NO_EDGE = 99;
/**
* 創建無向加權圖
*
* @param vexs 頂點數組
* @param edges 邊二維數組
*/
public ListDijkstraAndFloyd(E[] vexs, Edge<E>[] edges) {
this.edges = edges;
/*初始化頂點數組,並添加頂點*/
vertexs = new Node[vexs.length];
for (int i = 0; i < vertexs.length; i++) {
vertexs[i] = new Node<>(vexs[i], null);
}
/*初始化邊表,並添加邊節點到邊表尾部,即採用尾插法*/
for (Edge<E> edge : edges) {
// 讀取一條邊的起始頂點和結束頂點索引值
int p1 = getPosition(edge.from);
int p2 = getPosition(edge.to);
int weight = edge.weight;
/*這裏需要相互添加邊節點,無向圖可以看作相互可達的有向圖*/
// 初始化lnode1邊節點
LNode lnode1 = new LNode();
lnode1.vertex = p2;
lnode1.weight = weight;
// 將LNode鏈接到"p1所在鏈表的末尾"
if (vertexs[p1].firstLNode == null) {
vertexs[p1].firstLNode = lnode1;
} else {
linkLast(vertexs[p1].firstLNode, lnode1);
}
// 初始化lnode2邊節點
LNode lnode2 = new LNode();
lnode2.vertex = p1;
lnode2.weight = weight;
// 將lnode2鏈接到"p2所在鏈表的末尾"
if (vertexs[p2].firstLNode == null) {
vertexs[p2].firstLNode = lnode2;
} else {
linkLast(vertexs[p2].firstLNode, lnode2);
}
}
}
/**
* 獲取某條邊的某個頂點所在頂點數組的索引位置
*
* @param e 頂點的值
* @return 所在頂點數組的索引位置, 或者-1 - 表示不存在
*/
private int getPosition(E e) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i].data == e) {
return i;
}
}
return -1;
}
/**
* 將lnode節點鏈接到邊表的最後,採用尾插法
*
* @param first 邊表頭結點
* @param node 將要添加的節點
*/
private void linkLast(LNode first, LNode node) {
while (true) {
if (first.vertex == node.vertex) {
return;
}
if (first.nextLNode == null) {
break;
}
first = first.nextLNode;
}
first.nextLNode = node;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < vertexs.length; i++) {
stringBuilder.append(i).append("(").append(vertexs[i].data).append("): ");
LNode node = vertexs[i].firstLNode;
while (node != null) {
stringBuilder.append(node.vertex).append("(").append(vertexs[node.vertex].data).append("-").append(node.weight).append(")");
node = node.nextLNode;
if (node != null) {
stringBuilder.append("->");
} else {
break;
}
}
stringBuilder.append("\n");
}
return stringBuilder.toString();
}
/**
* 深度優先搜索遍歷圖的遞歸實現,類似於樹的先序遍歷
* 因此模仿樹的先序遍歷,同樣借用棧結構,這裏使用的是方法的遞歸,隱式的借用棧
*
* @param i 頂點索引
* @param visited 訪問標誌數組
*/
private void DFS(int i, boolean[] visited) {
//索引索引標記爲true ,表示已經訪問了
visited[i] = true;
System.out.print(vertexs[i].data + " ");
//獲取該頂點的邊表頭結點
LNode node = vertexs[i].firstLNode;
//循環遍歷該頂點的鄰接點,採用同樣的方式遞歸搜索
while (node != null) {
if (!visited[node.vertex]) {
DFS(node.vertex, visited);
}
node = node.nextLNode;
}
}
/**
* 深度優先搜索遍歷圖,類似於樹的前序遍歷,
*/
public void DFS() {
//新建頂點訪問標記數組,對應每個索引對應相同索引的頂點數組中的頂點
boolean[] visited = new boolean[vertexs.length];
//初始化所有頂點都沒有被訪問
for (int i = 0; i < vertexs.length; i++) {
visited[i] = false;
}
System.out.println("DFS: ");
System.out.print("\t");
/*循環搜索*/
for (int i = 0; i < vertexs.length; i++) {
//如果對應索引的頂點的訪問標記爲false,則搜索該頂點
if (!visited[i]) {
DFS(i, visited);
}
}
/*走到這一步,說明頂點訪問標記數組全部爲true,說明全部都訪問到了,深度搜索結束*/
System.out.println();
}
/**
* 廣度優先搜索圖,類似於樹的層序遍歷
* 因此模仿樹的層序遍歷,同樣借用隊列結構
*/
public void BFS() {
// 輔組隊列
Queue<Integer> indexLinkedList = new LinkedList<>();
//新建頂點訪問標記數組,對應每個索引對應相同索引的頂點數組中的頂點
boolean[] visited = new boolean[vertexs.length];
//初始化所有頂點都沒有被訪問
for (int i = 0; i < vertexs.length; i++) {
visited[i] = false;
}
System.out.println("BFS: ");
System.out.print("\t");
for (int i = 0; i < vertexs.length; i++) {
//如果訪問方劑爲false,則設置爲true,表示已經訪問,然後開始訪問
if (!visited[i]) {
visited[i] = true;
System.out.print(vertexs[i].data + " ");
indexLinkedList.add(i);
}
//判斷隊列是否有值,有就開始遍歷
if (!indexLinkedList.isEmpty()) {
//出隊列
Integer j = indexLinkedList.poll();
LNode node = vertexs[j].firstLNode;
while (node != null) {
int k = node.vertex;
if (!visited[k]) {
visited[k] = true;
System.out.print(vertexs[k].data + " ");
//繼續入隊列
indexLinkedList.add(k);
}
node = node.nextLNode;
}
}
}
System.out.println();
}
/**
* Prim算法求最小生成樹
*/
public void prim() {
System.out.println("prim: ");
//對應節點應該被連接的前驅節點,用來輸出
//默認爲0,即前驅結點爲第一個節點
int[] mid = new int[vertexs.length];
int start = 0;
int min, tmp, sum = 0;
int num = vertexs.length;
//頂點間邊的權值
//存儲未連接頂點到已連接頂點的最短距離(最小權)
int[] dis = new int[num];
// 初始化"頂點的權值數組",
// 將每個頂點的權值初始化爲"第start個頂點"到"該頂點"的權值。
//首先將其他頂點到0索引頂點的權值存儲進去
for (int i = 0; i < num; i++) {
dis[i] = getWeight(start, i);
}
//如果某頂點作爲末端頂點被連接,對應位置應該爲true
//第一個頂點默認被連接
boolean[] connected = new boolean[vertexs.length];
connected[0] = true;
/*默認第一個頂點已經找到了,因此最多還要需要大循環n-1次*/
for (int k = 1; k < num; k++) {
min = NO_EDGE;
//最小權值的頂點的索引
int minIndex = 0;
// 在未被加入到最小生成樹的頂點中,找出權值最小的頂點。
for (int i = 1; i < vertexs.length; i++) {
//排除已連接的頂點,排除權值等於0的值,因爲這裏默認頂點指向自己的權值爲0
if (!connected[i] && dis[i] != 0 && dis[i] < min) {
min = dis[i];
minIndex = i;
}
}
//如果沒找到,那麼該圖可能不是連通圖,直接返回了,此時最小生成樹沒啥意義
if (minIndex == 0) {
return;
}
//權值和增加
sum += min;
//該新連接頂點對應的索引值變成true,表示已被連接,後續判斷時跳過該頂點
connected[minIndex] = true;
//輸出對應的前驅頂點到該最小頂點的權值
System.out.println("\t" + vertexs[mid[minIndex]].data + " ---> " + vertexs[minIndex].data + " 權值:" + min);
/*在新頂點minIndex加入之前的其他所有頂點到連接頂點最小的權值已經計算過了
因此只需要更新其他頂點到新連接頂點minIndex是否還有更短的權值,有的話就更新找到距離已連接的頂點權最小的頂點*/
for (int i = 1; i < num; i++) {
//如果該頂點未連接
if (!connected[i]) {
// 獲取minindex頂點到未連接頂點i的權值
tmp = getWeight(minIndex, i);
/*如果新頂點到未連接頂點i的權值不爲0,並且比原始頂點到未連接頂點i的權值還要小,那麼更新對應位置的最小權值*/
if (tmp != 0 && dis[i] > tmp) {
dis[i] = tmp;
//更新前驅節點索引爲新加入節點索引
mid[i] = minIndex;
}
}
}
}
System.out.println("\t" + "sum: " + sum);
}
/**
* 嘗試獲取邊起點start到邊終點end的邊的權值,當然可能獲取不到
*
* @param start 邊起點
* @param end 邊終點
* @return 返回權值; 如果起點和終點相同則返回0;如果邊起點和邊終點之間並沒有邊, 則返回NO_EDGE
*/
private int getWeight(int start, int end) {
//如果start=end,則返回0
if (start == end) {
return 0;
}
//獲取該頂點的邊表的第一個值
LNode node = vertexs[start].firstLNode;
//循環查找邊表,看能否找到對應的索引=end,找不到就返回NO_EDGE,表示兩個頂點未連接。
while (node != null) {
if (end == node.vertex) {
return node.weight;
}
node = node.nextLNode;
}
return NO_EDGE;
}
/**
* Kruskal算法求最小生成樹,可以說鄰接矩陣和鄰接鏈表的實現方式是完全一致的
*/
public void kruskal() {
System.out.println("Kruskal: ");
//由於創建圖的時候保存了邊集數組,這裏直接使用就行了
//Edge[] edges = getEdges();
//this.edges=edges;
//對邊集數組進行排序
Arrays.sort(this.edges, Comparator.comparingInt(o -> o.weight));
// 用於保存已有最小生成樹中每個頂點在該最小樹中的最終終點的索引
int[] vends = new int[this.edges.length];
//能夠知道終點索引範圍是[0,this.edges.length-1],因此填充edges.length表示沒有終點
Arrays.fill(vends, this.edges.length);
int sum = 0;
for (Edge<E> edge : this.edges) {
// 獲取第i條邊的起點索引from
int from = getPosition(edge.from);
// 獲取第i條邊的終點索引to
int to = getPosition(edge.to);
// 獲取頂點from在"已有的最小生成樹"中的終點
int m = getEndIndex(vends, from);
// 獲取頂點to在"已有的最小生成樹"中的終點
int n = getEndIndex(vends, to);
// 如果m!=n,意味着沒有形成環路,則可以添加,否則直接跳過,進行下一條邊的判斷
if (m != n) {
//添加設置原始終點索引m在已有的最小生成樹中的終點爲n
vends[m] = n;
System.out.println("\t" + vertexs[from].data + " ---> " + vertexs[to].data + " 權值:" + edge.weight);
sum += edge.weight;
}
}
System.out.println("\t" + "sum: " + sum);
//System.out.println(Arrays.toString(this.edges));
}
/**
* 獲取頂點索引i的終點如果沒有終點則返回頂點索引本身
*
* @param vends 頂點在最小生成樹中的終點
* @param i 頂點索引
* @return 頂點索引i的終點如果沒有終點則返回頂點索引本身
*/
private int getEndIndex(int[] vends, int i) {
//這裏使用循環查找的邏輯,尋找的是最終的終點
while (vends[i] != this.edges.length) {
i = vends[i];
}
return i;
}
/**
* 如果沒有現成的邊集數組,那麼根據鄰接表結構獲取圖中的邊集數組
*
* @return 圖的邊集數組
*/
private Edge[] getEdges() {
List<Edge> edges = new ArrayList<>();
//遍歷頂點數組
for (int i = 0; i < vertexs.length; i++) {
LNode node = vertexs[i].firstLNode;
while (node != null) {
//只需添加起點索引小於終點索引的邊就行了
if (node.vertex > i) {
edges.add(new Edge<>(vertexs[i].data, vertexs[node.vertex].data, node.weight));
}
node = node.nextLNode;
}
}
return edges.toArray(new Edge[0]);
}
/**
* Dijkstra算法求最短路徑。
*
* @param start 起始頂點索引。即計算"頂點vs"到其它頂點的最短路徑。
*/
public void dijkstra(int start) {
checkIndex(start);
int[] distance = getShortestDistance(start, vertexs.length);
// 打印dijkstra最短路徑的結果
System.out.println("dijkstra(" + vertexs[start].data + "):");
for (int i = 0; i < vertexs.length; i++) {
System.out.println("\t(" + vertexs[start].data + " ---> " + vertexs[i].data + ")最短路徑:" + distance[i]);
}
}
/**
* Dijkstra算法求最短路徑。
*
* @param start 起始頂點索引。
* @param end 結束點索引
*/
public void dijkstra(int start, int end) {
checkIndex(start, end);
int[] shortestPathance = getShortestDistance(start, end);
// 打印dijkstra最短路徑的結果
System.out.println("Dijkstra(" + vertexs[start].data + " ---> " + vertexs[end].data + ")最短路徑:" + shortestPathance[end]);
}
/**
* Dijkstra算法求最短路徑
*
* @param start 起始點
* @param end 終點,如果end=vertexs.length說明是遍歷查找所有的最短路徑
* @return 起始頂點到其他點或者指定點的最短權值
*/
private int[] getShortestDistance(int start, int end) {
/*1、該數組存放起始頂點到其他點的權值*/
int[] distance = new int[vertexs.length];
//初始化數據
for (int i = 0; i < vertexs.length; i++) {
//首先設置起始點到頂點i到的最短路徑爲起始點到頂點i的權。
distance[i] = getWeight(start, i);
}
/*2、標誌位數組.某個位置表示true表示,對應位置的頂點到起始頂點的最短路徑已成功獲取。*/
boolean[] shortest = new boolean[vertexs.length];
//首先設置起始點到自己的的路徑已經找到了,爲0
shortest[start] = true;
/*3、最多遍歷vertexs.length-1次;每次找出起始點到一個頂點的最短路徑。*/
int k;
int min;
for (int i = 1; i < vertexs.length; i++) {
k = 0;
// 尋找當前最小的路徑;
min = NO_EDGE;
for (int j = 0; j < vertexs.length; j++) {
//排除已經找到的最短路徑之後,找到離start最近的頂點(k)。
if (!shortest[j] && distance[j] < min) {
min = distance[j];
k = j;
}
}
//先設置起始點到新頂點k的最短路徑已經找到
shortest[k] = true;
if (end != vertexs.length && k == end) {
break;
}
//更新未獲取最短路徑的頂點的最短路徑,因爲其他已找到的頂點的最短路徑已經找到了,這裏指需要更新新加入的已找到的可達頂點的路徑.
for (int j = 0; j < vertexs.length; j++) {
int tmp = getWeight(k, j);
//排除已經找到的最短路徑,排除未連接的路徑,排除等於0的路徑(連接自己)之後
//找到離start最如果新的最短路徑比以前的最短路徑還要短,則更新最短路徑。
if (!shortest[j] && tmp != NO_EDGE && tmp != 0 && ((tmp = min + tmp) < distance[j])) {
distance[j] = tmp;
}
}
}
return distance;
}
/**
* 索引檢查
*
* @param index 多個索引
*/
private void checkIndex(int... index) {
for (int i : index) {
if (i < 0 || i >= vertexs.length) {
throw new ArrayIndexOutOfBoundsException("索引越界:" + i);
}
}
}
/**
* Floyd算法獲取所有頂點到所有頂點的最短路徑,與鄰接矩陣的實現基本一致
*/
public void floyd() {
//路徑矩陣(兩頂點最短路徑,即最小權值)
int[][] shortestPath = new int[vertexs.length][vertexs.length];
/*初始化數據*/
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
//獲取兩點的直接權值
//如果是頻繁調用該方法,因此可以創建一個屬於對象的權值矩陣用來保存權值,這裏爲了簡單沒做
shortestPath[i][j] = getWeight(i, j);
}
}
// 計算最短路徑
for (int k = 0; k < vertexs.length; k++) {
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
//要求經過下標k頂點的兩個路徑都不能等於NO_EDGE,否則就是沒有路徑,NO_EDGE應該選取的足夠的大,否則可能出錯
int tmp = (shortestPath[i][k] == NO_EDGE || shortestPath[k][j] == NO_EDGE) ? NO_EDGE : (shortestPath[i][k] + shortestPath[k][j]);
// 如果經過下標爲k頂點路徑比原兩點間路徑更短,則更新shortestPath[i][j]
if (shortestPath[i][j] > tmp) {
// i到j最短路徑對應的值設爲經過k的更小的一個
shortestPath[i][j] = tmp;
}
}
}
}
/*輸出路徑矩陣*/
System.out.println("Floyd: ");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.print("\t" + shortestPath[i][j]);
}
System.out.println();
}
}
public static void main(String[] args) {
//頂點數組
Character[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
//邊數組,加權值
Edge[] edges = {
new Edge<>('A', 'C', 8),
new Edge<>('D', 'A', 2),
new Edge<>('A', 'F', 3),
new Edge<>('B', 'C', 4),
new Edge<>('C', 'D', 5),
new Edge<>('E', 'G', 6),
new Edge<>('E', 'B', 7),
new Edge<>('D', 'B', 9),
new Edge<>('F', 'G', 9)};
//構建圖
ListDijkstraAndFloyd<Character> listDijkstraAndFloyd = new ListDijkstraAndFloyd<Character>(vexs, edges);
//輸出圖
System.out.println(listDijkstraAndFloyd);
//深度優先遍歷
//DFS:
//A C B E G F D
listDijkstraAndFloyd.DFS();
//廣度優先遍歷
//BFS:
//A C D F B G E
listDijkstraAndFloyd.BFS();
//Prim算法求最小生成樹
listDijkstraAndFloyd.prim();
//Kruskal算法求最小生成樹
listDijkstraAndFloyd.kruskal();
// Dijkstra算法獲取某個索引的頂點到其它各個頂點的最短距離
// 這裏參數是索引,也可以是一個頂點,需要稍微修改代碼獲取頂點的索引,比較簡單這裏就不做了
listDijkstraAndFloyd.dijkstra(0);
// Dijkstra算法獲取一個頂點到另一個頂點的最短距離
listDijkstraAndFloyd.dijkstra(2, 0);
// Floyd算法獲取所有頂點到所有頂點的最短路徑
listDijkstraAndFloyd.floyd();
}
}
參考
《算法》
《數據結構與算法》
《大話數據結構》
《算法圖解》
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!