一,問題描述
給定一個有向圖G=(V,E),將之進行拓撲排序,如果圖有環,則提示異常。
要想實現圖的算法,如拓撲排序、最短路徑……並運行看輸出結果,首先就得構造一個圖。由於構造圖的方式有很多種,這裏假設圖的數據存儲在一個文件中,
每一行包含如下的信息:
LinkID,SourceID,DestinationID,Cost
其中,LinkID爲該有向邊的索引,SourceID爲該有向邊的起始頂點的索引,DestinationID爲該有向邊的終止頂點的索引,Cost爲該有向邊的權重。
0,0,1,1
1,0,2,2
2,0,3,1
3,2,1,3
4,3,1,1
5,2,3,1
6,3,2,1
(以上示例引用自網上,該圖僅用來表示存儲圖信息的文件內容的格式,對拓撲排序而言,上圖顯然存在環)
對於以下的拓撲排序程序,只用到了SourceID,和DestionatinID這兩個字段。拓撲序列以頂點的索引表示。後續會實現無向圖的最短路徑算法,就會用到Cost這個字段啦!!!
二,算法實現思路
拓撲排序,其實就是尋找一個入度爲0的頂點,該頂點是拓撲排序中的第一個頂點序列,將之標記刪除,然後將與該頂點相鄰接的頂點的入度減1,再繼續尋找入度爲0的頂點,直至所有的頂點都已經標記刪除或者圖中有環。
從上可以看出,關鍵是尋找入度爲0的頂點。
一種方式是遍歷整個圖中的頂點,找出入度爲0的頂點,然後標記刪除該頂點,更新相關頂點的入度,由於圖中有V個頂點,每次找出入度爲0的頂點後會更新相關頂點的入度,因此下一次又要重新掃描圖中所有的頂點。故時間複雜度爲O(V^2)
由於刪除入度爲0的頂點時,只會更新與它鄰接的頂點的入度,即只會影響與之鄰接的頂點。但是上面的方式卻遍歷了圖中所有的頂點的入度。
改進的另一種方式是:先將入度爲0的頂點放在棧或者隊列中。當隊列不空時,刪除一個頂點v,然後更新與頂點v鄰接的頂點的入度。只要有一個頂點的入度降爲0,則將之入隊列。此時,拓撲排序就是頂點出隊的順序。該算法的時間複雜度爲O(V+E)
三,拓撲排序方法的實現
該算法藉助隊列來實現時,感覺與 二叉樹的 層序遍歷算法很相似啊。說明這裏面有廣度優先的思想。
第一步:遍歷圖中所有的頂點,將入度爲0的頂點 入隊列。
第二步:從隊列中出一個頂點,打印頂點,更新該頂點的鄰接點的入度(減1),如果鄰接點的入度減1之後變成了0,則將該鄰接點入隊列。
第三步:一直執行上面 第二步,直到隊列爲空。
1 public void topoSort() throws Exception{ 2 int count = 0;//判斷是否所有的頂點都出隊了,若有頂點未入隊(組成環的頂點),則這些頂點肯定不會出隊 3 4 Queue<Vertex> queue = new LinkedList<>();// 拓撲排序中用到的棧,也可用隊列. 5 //掃描所有的頂點,將入度爲0的頂點入隊列 6 Collection<Vertex> vertexs = directedGraph.values(); 7 for (Vertex vertex : vertexs) 8 if(vertex.inDegree == 0) 9 queue.offer(vertex); 10 //度爲0的頂點出隊列並且更新它的鄰接點的入度 11 while(!queue.isEmpty()){ 12 Vertex v = queue.poll(); 13 System.out.print(v.vertexLabel + " ");//輸出拓撲排序的順序 14 count++; 15 for (Edge e : v.adjEdges) 16 if(--e.endVertex.inDegree == 0) 17 queue.offer(e.endVertex); 18 } 19 if(count != directedGraph.size()) 20 throw new Exception("Graph has circle"); 21 }
第7行for循環:先將圖中所有入度爲0的頂點入隊列。
第11行while循環:將入度爲0的頂點出隊列,並更新與之鄰接的頂點的入度,若鄰接頂點的入度降爲0,則入隊列(第16行if語句)。
第19行if語句判斷圖中是否有環。因爲,只有在每個頂點出隊時,count++。對於組成環的頂點,是不可能入隊列的,因爲組成環的頂點的入度不可能爲0(第16行if語句不會成立).
因此,如果有環,count的值 一定小於圖中頂點的個數。
四,完整代碼實現
DirectedGraph.java中定義了圖 數據結構,(圖的實現可參考:數據結構--圖 的JAVA實現(上))。並根據FileUtil.java中得到的字符串構造圖。
構造 圖之後,topoSort方法實現了拓撲排序。
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 DirectedGraph { 12 13 private class Vertex{ 14 private String vertexLabel;// 頂點標識 15 private List<Edge> adjEdges; 16 private int inDegree;// 該頂點的入度 17 18 public Vertex(String verTtexLabel) { 19 this.vertexLabel = verTtexLabel; 20 inDegree = 0; 21 adjEdges = new LinkedList<Edge>(); 22 } 23 } 24 25 private class Edge { 26 private Vertex endVertex; 27 28 // private double weight; 29 public Edge(Vertex endVertex) { 30 this.endVertex = endVertex; 31 } 32 } 33 34 private Map<String, Vertex> directedGraph; 35 36 public DirectedGraph(String graphContent) { 37 directedGraph = new LinkedHashMap<String, DirectedGraph.Vertex>(); 38 buildGraph(graphContent); 39 } 40 41 private void buildGraph(String graphContent) { 42 String[] lines = graphContent.split("\n"); 43 Vertex startNode, endNode; 44 String startNodeLabel, endNodeLabel; 45 Edge e; 46 for (int i = 0; i < lines.length; i++) { 47 String[] nodesInfo = lines[i].split(","); 48 startNodeLabel = nodesInfo[1]; 49 endNodeLabel = nodesInfo[2]; 50 startNode = directedGraph.get(startNodeLabel); 51 if(startNode == null){ 52 startNode = new Vertex(startNodeLabel); 53 directedGraph.put(startNodeLabel, startNode); 54 } 55 endNode = directedGraph.get(endNodeLabel); 56 if(endNode == null){ 57 endNode = new Vertex(endNodeLabel); 58 directedGraph.put(endNodeLabel, endNode); 59 } 60 61 e = new Edge(endNode);//每讀入一行代表一條邊 62 startNode.adjEdges.add(e);//每讀入一行數據,起始頂點添加一條邊 63 endNode.inDegree++;//每讀入一行數據,終止頂點入度加1 64 } 65 } 66 67 public void topoSort() throws Exception{ 68 int count = 0; 69 70 Queue<Vertex> queue = new LinkedList<>();// 拓撲排序中用到的棧,也可用隊列. 71 //掃描所有的頂點,將入度爲0的頂點入隊列 72 Collection<Vertex> vertexs = directedGraph.values(); 73 for (Vertex vertex : vertexs) 74 if(vertex.inDegree == 0) 75 queue.offer(vertex); 76 77 while(!queue.isEmpty()){ 78 Vertex v = queue.poll(); 79 System.out.print(v.vertexLabel + " "); 80 count++; 81 for (Edge e : v.adjEdges) 82 if(--e.endVertex.inDegree == 0) 83 queue.offer(e.endVertex); 84 } 85 if(count != directedGraph.size()) 86 throw new Exception("Graph has circle"); 87 } 88 }
FileUtil.java負責從文件中讀取圖的信息。將文件內容轉換成 第一點 中描述的字符串格式。--該類來源於網絡
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.Closeable; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public final class FileUtil { /** * 讀取文件並按行輸出 * @param filePath * @param spec 允許解析的最大行數, spec==null時,解析所有行 * @return * @author * @since 2016-3-1 */ public static String read(final String filePath, final Integer spec) { File file = new File(filePath); // 當文件不存在或者不可讀時 if ((!isFileExists(file)) || (!file.canRead())) { System.out.println("file [" + filePath + "] is not exist or cannot read!!!"); return null; } BufferedReader br = null; FileReader fb = null; StringBuffer sb = new StringBuffer(); try { fb = new FileReader(file); br = new BufferedReader(fb); String str = null; int index = 0; while (((spec == null) || index++ < spec) && (str = br.readLine()) != null) { sb.append(str + "\n"); // System.out.println(str); } } catch (IOException e) { e.printStackTrace(); } finally { closeQuietly(br); closeQuietly(fb); } return sb.toString(); } /** * 寫文件 * @param filePath 輸出文件路徑 * @param content 要寫入的內容 * @param append 是否追加 * @return * @author s00274007 * @since 2016-3-1 */ public static int write(final String filePath, final String content, final boolean append) { File file = new File(filePath); if (content == null) { System.out.println("file [" + filePath + "] invalid!!!"); return 0; } // 當文件存在但不可寫時 if (isFileExists(file) && (!file.canRead())) { return 0; } FileWriter fw = null; BufferedWriter bw = null; try { if (!isFileExists(file)) { file.createNewFile(); } fw = new FileWriter(file, append); bw = new BufferedWriter(fw); bw.write(content); } catch (IOException e) { e.printStackTrace(); return 0; } finally { closeQuietly(bw); closeQuietly(fw); } return 1; } private static void closeQuietly(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException e) { } } private static boolean isFileExists(final File file) { if (file.exists() && file.isFile()) { return true; } return false; } }
測試類:TestTopoSort.java
1 public class TestTopoSort { 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 DirectedGraph directedGraph = new DirectedGraph(graphContent); 11 try{ 12 directedGraph.topoSort(); 13 }catch(Exception e){ 14 System.out.println("graph has circle"); 15 e.printStackTrace(); 16 } 17 } 18 }