一,問題描述
給出一個無向圖,指定無向圖中某個頂點作為源點。求出圖中所有頂點到源點的最短路徑。
無向圖的最短路徑其實是源點到該頂點的最少邊的數目。
本文假設圖的信息保存在文件中,通過讀取文件來構造圖。文件內容的格式參考這篇文章第一部分。
二,算法實現思路
無向圖的最短路徑實現相對於帶權的有向圖最短路徑實現要簡單得多。
源點的最短路徑距離為0,從源點開始,采用廣度優先的順序,首先將與源點鄰接的頂點的路徑求出,然后再依次求解圖中其他頂點的最短路徑。
由於頂點的最短路徑的求解順序 是一個 廣度優先的順序,因此需要一個輔助隊列。初始時,將源點的最短路徑距離設置為0,將源點入隊列。
然后,在一個while循環中,從隊列中彈出頂點,遍歷該頂點的鄰接點,若該鄰接點的距離未被更新過(表示該鄰接點未被訪問過),更新鄰接點的最短路徑距離為 該頂點的距離加上1,並將所有的鄰接點入隊列。
三,最短路徑算法的實現
感覺該算法的實現與 二叉樹的層序遍歷,有向圖的拓撲排序算法實現都非常的相似。他們都采用了廣度的思想在里面。
廣度優先的思想就是:處理完某個頂點后,去處理該頂點的所有鄰接點,處理完它的鄰接點后,再去處理更遠(更外層)的頂點。
算法的代碼如下:
1 /* 2 * 計算源點s到無向圖中各個頂點的最短路徑 3 * 需要一個隊列來保存圖中的頂點,初始時,源點入隊列,然后以廣度的形式向外擴散求解其他頂點的最短路徑 4 */ 5 private void unweightedShortestPath(Vertex s){ 6 //初始化 7 Queue<Vertex> queue = new LinkedList<>(); 8 s.dist = 0; 9 queue.offer(s);//將源點dist設置為0並入隊列 10 11 while(!queue.isEmpty()){ 12 Vertex v = queue.poll(); 13 for (Edge e : v.adjEdges) {//掃描v的鄰接邊(點) 14 if(e.endVertex.dist == Integer.MAX_VALUE){//如果這個頂點(e.endVertex)未被訪問(每個頂點只會入隊列一次) 15 e.endVertex.dist = v.dist + 1;//更新該頂點到源點的距離 16 queue.offer(e.endVertex); 17 e.endVertex.preNode = v;//設置該頂點的前驅頂點 18 }//end if 19 }//end for 20 }//end while 21 }
第11行while循環,每個頂點出隊列一次,第13行for循環,表示每條邊被處理一次,故算法的時間復雜度為O(V+E)
第14行if語句表明,圖中每個頂點只會入隊列一次。因為,頂點入隊列后,該頂點的 dist 設置為 v.dist+1,不再是 Integer.MAX_VALUE
四,完整代碼實現
NonDirectedGraph.java構造圖並實現最短路徑算法
1 import java.util.Collection; 2 import java.util.LinkedHashMap; 3 import java.util.LinkedList; 4 import java.util.List; 5 import java.util.Map; 6 import java.util.Queue; 7 8 /* 9 * 求解無向圖的單源最短路徑 10 */ 11 public class NonDirectedGraph { 12 private class Vertex{ 13 private String vertexLabel;//頂點標識 14 private List<Edge> adjEdges;//與該頂點鄰接的邊(點) 15 private int dist;//頂點距離(該頂點到起始頂點的距離) 16 private Vertex preNode; 17 18 public Vertex(String vertexLabel) { 19 this.vertexLabel = vertexLabel; 20 adjEdges = new LinkedList<>(); 21 dist = Integer.MAX_VALUE; 22 preNode = null; 23 } 24 } 25 private class Edge{ 26 private Vertex endVertex; 27 public Edge(Vertex endVertex) { 28 this.endVertex = endVertex; 29 } 30 } 31 32 private Map<String, Vertex> nonDirectedGraph;//保存了圖中所有的頂點,邊的關系以List形式保存在Vertex類中 33 private Vertex startVertex;//圖的起始頂點 34 35 public NonDirectedGraph(String graphContent) { 36 nonDirectedGraph = new LinkedHashMap<>(); 37 buildGraph(graphContent); 38 } 39 40 private void buildGraph(String graphContent){ 41 String[] lines = graphContent.split("\n"); 42 43 String startNodeLabel, endNodeLabel; 44 Vertex startNode, endNode; 45 for(int i = 0; i < lines.length; i++){ 46 String[] nodesInfo = lines[i].split(","); 47 startNodeLabel = nodesInfo[1]; 48 endNodeLabel = nodesInfo[2]; 49 50 endNode = nonDirectedGraph.get(endNodeLabel); 51 if(endNode == null){ 52 endNode = new Vertex(endNodeLabel); 53 nonDirectedGraph.put(endNodeLabel, endNode); 54 } 55 56 startNode = nonDirectedGraph.get(startNodeLabel); 57 if(startNode == null){ 58 startNode = new Vertex(startNodeLabel); 59 nonDirectedGraph.put(startNodeLabel, startNode); 60 } 61 Edge e = new Edge(endNode); 62 //對於無向圖而言,起點和終點都要添加邊 63 endNode.adjEdges.add(e); 64 startNode.adjEdges.add(e); 65 } 66 startVertex = nonDirectedGraph.get(lines[0].split(",")[1]);//總是以文件中第一行第二列的那個標識頂點作為源點 67 } 68 69 public void unweightedShortestPath(){ 70 unweightedShortestPath(startVertex); 71 } 72 73 74 /* 75 * 計算源點s到無向圖中各個頂點的最短路徑 76 * 需要一個隊列來保存圖中的頂點,初始時,源點入隊列,然后以廣度的形式向外擴散求解其他頂點的最短路徑 77 */ 78 private void unweightedShortestPath(Vertex s){ 79 //初始化 80 Queue<Vertex> queue = new LinkedList<>(); 81 s.dist = 0; 82 queue.offer(s);//將源點dist設置為0並入隊列 83 84 while(!queue.isEmpty()){ 85 Vertex v = queue.poll(); 86 for (Edge e : v.adjEdges) {//掃描v的鄰接邊(點) 87 if(e.endVertex.dist == Integer.MAX_VALUE){//如果這個頂點(e.endVertex)未被訪問(每個頂點只會入隊列一次) 88 e.endVertex.dist = v.dist + 1;//更新該頂點到源點的距離 89 queue.offer(e.endVertex); 90 e.endVertex.preNode = v;//設置該頂點的前驅頂點 91 }//end if 92 }//end for 93 }//end while 94 } 95 96 //打印圖中所有頂點到源點的距離及路徑 97 public void showDistance(){ 98 Collection<Vertex> vertexs = nonDirectedGraph.values(); 99 for (Vertex vertex : vertexs) { 100 System.out.print(vertex.vertexLabel + "<--"); 101 Vertex tmpPreNode = vertex.preNode; 102 while(tmpPreNode != null){ 103 System.out.print(tmpPreNode.vertexLabel + "<--"); 104 tmpPreNode = tmpPreNode.preNode; 105 } 106 System.out.println("distance=" + vertex.dist); 107 } 108 } 109 }
打印路徑也可以使用遞歸來實現:
1 public void showDistanceRecursive(Vertex v){ 2 if(v.preNode != null){ 3 showDistanceRecursive(v.preNode); 4 } 5 System.out.print(v.vertexLabel + " "); 6 }
打印頂點 v 的路徑,第三行 先打印 v 的前驅頂點的路徑,然后再在第5行打印 v 。
第5行的打印輸出語句在第三行的遞歸調用語句之后,故最里層的遞歸調用最先被打印出來,最里層的遞歸調用即源點,因為只有源點的 preNode == null。
當所有的里層遞歸調用返回后,最終執行到最外層的遞歸調用處,執行第5行打印 頂點 v 后,整個遞歸結束。
TestShortestPath.java是個測試類,用來測試結果。
1 public class TestShortestPath {//hapjin test 2 public static void main(String[] args) { 3 String graphFilePath; 4 if(args.length == 0) 5 graphFilePath = "F:\\xxx"; 6 else 7 graphFilePath = args[0]; 8 9 String graphContent = FileUtil.read(graphFilePath, null); 10 NonDirectedGraph graph = new NonDirectedGraph(graphContent); 11 graph.unweightedShortestPath(); 12 graph.showDistance(); 13 } 14 }
FileUtil.java負責讀取存儲圖信息的文件。具體參考 有向圖的拓撲排序算法JAVA實現
保存圖的 文件內容如下:
0,0,1,4
1,0,2,7
2,0,3,3
3,1,2,3
4,1,4,2
5,3,4,3
6,2,5,2
7,4,5,2
測試輸出結果如下:
源點標識是 0,
0 號頂點到 1 號頂點的最短距離為1,路徑為:0-->1
0 號頂點到 5 號頂點的最短距離為2,路徑為:0-->2-->5
.....
....