有向圖的拓撲排序算法JAVA實現

一,問題描述

給定一個有向圖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 }
複製代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章