數據結構--圖 的JAVA實現(下)

上一篇文章中記錄瞭如何實現圖的鄰接表。本文藉助上一篇文章實現的鄰接表來表示一個有向無環圖。

1,概述

圖的實現與鄰接表的實現最大的不同就是,圖的實現需要定義一個數據結構來存儲所有的頂點以及能夠對圖進行什麼操作,而鄰接表的實現重點關注的圖中頂點的實現,即怎麼定義JAVA類來表示頂點,以及能夠對頂點進行什麼操作。

爲了存儲圖中所有的頂點,定義了一個Map<key, value>,實際實現爲LinkedHashMap<T, VertexInterface<T>>,key 爲 頂點的標識,key 是泛型,這樣就可以用任意數據類型來標識頂點了,如String、Integer……

value 當然就是表示頂點的類了,因爲我們需要存儲的是頂點嘛。即value 爲 VertexInterface<T> 。這裏爲什麼不用List而用Map來存儲頂點呢?用Map的好處就是方便查詢頂點,即可以用頂點標識來查找頂點。這也是爲了方便後面實現圖的DFS、BFS 等算法而考慮的。

此外,還定義了一個整型變量 edgeCount 用來保存圖中邊的數目,這也是必要的。討論一個圖,當然要有圖的頂點,由Map保存,頂點數目可以通過 Map.size() 方法獲得;也要有邊,而邊已經隱含在Vertex.java中了(具體參考上一篇文章),因此這裏只定義一個保存圖中邊的總數的變量即可。圖的定義 部分代碼如下:

public class DirectedGraph<T> implements GraphInterface<T>,java.io.Serializable{

    private static final long serialVersionUID = 1L;

    private Map<T, VertexInterface<T>> vertices;//map 對象用來保存圖中的所有頂點.T 是頂點標識,VertexInterface爲頂點對象
    private int edgeCount;//記錄圖中 邊的總數
    
    public DirectedGraph() {
        vertices = new LinkedHashMap<>();//按頂點的插入順序保存頂點
    }

2,圖的基本操作

這裏的基本操作不是對圖進行DFS、BFS、拓撲排序、求最短路徑……而是一系列的如何構造圖的方法,這些方法是實現圖的遍歷、求最短路徑、拓撲排序的基礎。

在 1 中說明了用Map保存圖的頂點,那麼如何把頂點對象添加到Map中呢?

public void addVertex(T vertexLabel) {
         //若頂點相同時,新插入的頂點將覆蓋原頂點,這是由LinkedHashMap的put方法決定的
         //每添加一個頂點,會創建一個LinkedList列表,它存儲該頂點對應的鄰接點,或者說是與該頂點相關聯的邊
         vertices.put(vertexLabel, new Vertex(vertexLabel));//new Vertex 對象,會創建一個LinkedList,該LinkedList用來表示該頂點的鄰接表
     }

如何表示圖中兩個頂點之間的邊呢?

public boolean addEdge(T begin, T end, double edgeWeight) {
        boolean result = false;
        VertexInterface<T> beginVertex = vertices.get(begin);//獲得表示邊的起始頂點
        VertexInterface<T> endVertex = vertices.get(end);//獲得表示 邊的終點
        
        if(beginVertex != null && endVertex != null)
            result = beginVertex.connect(endVertex, edgeWeight);//起始點與終點連接,即成一條邊
        if(result)
            edgeCount++;
        return result;//當添加重複邊時會返回 false
    }

3,圖的相關算法的JAVA實現及分析

正如上一篇文章中的總結提到:算法的實現依賴於採用了何種數據結構,依賴於數據結構--圖的具體實現。由於這裏的數據結構--圖的實現與《算法導論》中描述的圖的數據結構有一點差別,如:沒有定義表示圖的訪問狀態的"白色頂點、灰色頂點、黑色頂點",因此算法的實現也與《算法導論》中算法的實現有輕微的差別。

廣度優先遍歷算法與最短路徑算法很相似,對廣度優先遍歷算法稍加修改,就可以變成最短路徑算法了。

理解:

深度優先遍歷算法與拓撲算法也很相似,拓撲排序算法的實現可以藉助深度優先遍歷算法。

理解:

具體參考《算法導論》

①廣度優先遍歷算法:若頂點A先於頂點B被訪問,則頂點A的鄰接點也先於頂點B的鄰接點被訪問。特點:先把起始頂點附近的頂點訪問完,再訪問遠處的頂點。

在廣度優先遍歷算法的具體實現中,需要兩個隊列。一個輔助遍歷,保存遍歷過程中遇到的頂點,當訪問完成了某個頂點A後,將A出隊列,緊接着將A的所有鄰接點都入隊列,並訪問

另一個隊列用來保存訪問的順序,前一個隊列的頂點入隊順序就是圖的廣度遍歷順序,因此,該隊列保持 與 前一個隊列的頂點入隊操作 一致。由於前一個隊列是輔助遍歷的,它有出隊的操作,它就不能記錄整個頂點的訪問序列了,因此才需要一個保存訪問順序的隊列。當整個過程遍歷完成後,將 保存訪問順序的隊列 進行出隊操作,即可得到整個圖的廣度優先遍歷的順序了。具體算法如下:

public Queue<T> getBreadthFirstTraversal(T origin) {//origin 標識遍歷的初始頂點
        resetVertices();//將頂點的必要數據域初始化,複雜度爲O(V)
        Queue<VertexInterface<T>> vertexQueue = new LinkedList<>();//保存遍歷過程中遇到的頂點,它是輔助遍歷的,有出隊列操作
        Queue<T> traversalOrder = new LinkedList<>();//保存遍歷過程中遇到的 頂點標識--整個圖的遍歷順序就保存在其中,無出隊操作
        VertexInterface<T> originVertex = vertices.get(origin);//根據頂點標識獲得初始遍歷頂點
        originVertex.visit();//訪問該頂點
        traversalOrder.offer(originVertex.getLabel());
        vertexQueue.offer(originVertex);
        
        while(!vertexQueue.isEmpty()){
            VertexInterface<T> frontVertex = vertexQueue.poll();//出隊列,poll()在隊列爲空時返回null
            Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
            while(neighbors.hasNext())//對於 每個頂點都遍歷了它的鄰接表,即遍歷了所有的邊,複雜度爲O(E)
            {
                VertexInterface<T> nextNeighbor = neighbors.next();
                if(!nextNeighbor.isVisited()){
                    nextNeighbor.visit();//廣度優先遍歷未訪問的頂點
                    traversalOrder.offer(nextNeighbor.getLabel());
                    vertexQueue.offer(nextNeighbor);//將該頂點的鄰接點入隊列
                }
            }//end inner while
        }//end outer while
        return traversalOrder;
    }

從中可以看出,該算法的時間複雜度爲--遍歷之前,給每個頂點進行初始化時需要遍歷所有頂點V,在遍歷過程中需要判斷頂點的鄰接點是否被遍歷,也即遍歷該頂點的鄰接表,鄰接表代表的實質是邊,邊總數爲E,故總的時間複雜度爲O(V+E),空間複雜度爲O(V)--輔助隊列的長度爲頂點的長度

 

②最短路徑算法:在邊不帶權值的圖中求頂點A到頂點B的最短路徑--其實就是頂點A到頂點B之間的最少邊的條數

 調用最短路徑算法之前,首先要確定一個初始頂點,圖中其他頂點的路徑長度都是相對於初始頂點而言的。求兩個頂點間最短路徑,其實並不是找出兩個頂點間所有的路徑長度,然後取最小值。而是藉助於廣度優先遍歷算法,將每個頂點相對於初始頂點的最短路徑長度保存在 cost 屬性中,廣度優先算法的性質保證了頂點間的路徑是最短的。在最短路徑的計算中,設初始點爲 i,頂點A相對於初始點的最短路徑長度爲 length,則 頂點A的鄰接點 相對於初始頂點 i 的最短長度爲 length+1.

因此,執行最短路徑算法後,實際上求得了圖中所有頂點相對於初始頂點的最短路徑。

初始頂點的路徑長度爲0(每個頂點有一個 cost 屬性---見上一文章分析,由 cost 來記錄每個頂點相對於初始頂點的路徑長度)。因此,獲得某頂點的最短路徑只需要調用它的getCost方法即可。

 最短路徑算法的代碼如下,可以看出它和廣度優先算法的代碼非常的相似,其實就是廣度優先算法的應用而已。

public int getShortestPath(T begin, T end, Stack<T> path) {
        resetVertices();//圖中頂點的初始化
        boolean done = false;//標記整個遍歷過程是否完成
        Queue<VertexInterface<T>> vertexQueue = new LinkedList<>();//輔助隊列,保存遍歷過程中遇到的頂點
        VertexInterface<T> beginVertex = vertices.get(begin);//獲得起始頂點
        VertexInterface<T> endVertex = vertices.get(end);//獲得終點,求起始頂點到終點的最短路徑
        
        beginVertex.visit();
        vertexQueue.offer(beginVertex);//起始頂點入隊列
        //Assertion: resetVertices() 已經對 beginVertex 執行了 setCost(0)
        
        while(!done && !vertexQueue.isEmpty()){//while循環完成後,實際上求得了圖中所有頂點相對於初始點的 cost 屬性值
            VertexInterface<T> frontVertex = vertexQueue.poll();
            Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
            while(!done && neighbors.hasNext()){//計算 frontVertex的所有鄰接頂點的 路徑長度
                VertexInterface<T> nextNeighbor = neighbors.next();
                if(!nextNeighbor.isVisited()){
                    nextNeighbor.visit();
                    nextNeighbor.setPredecessor(frontVertex);//設置frontVertex 的前驅頂點
                    nextNeighbor.setCost(frontVertex.getCost() + 1);//該頂點的路徑長度是 它的前驅頂點的路徑長度+1
                    vertexQueue.offer(nextNeighbor);
                }//end if
                
                if(nextNeighbor.equals(endVertex))
                    done = true;
            }//end inner while
        }//end outer while. and traverse over
        
        int pathLength = (int)endVertex.getCost();//初始頂點的 cost爲 0,每個頂點的 cost 屬性記錄了它相對於初始頂點的最短長度
        path.push(endVertex.getLabel());
        
        VertexInterface<T> vertex = endVertex;
        while(vertex.hasPredecessor()){
            vertex = vertex.getPredecessor();
            path.push(vertex.getLabel());
        }
        return pathLength;
    }

③深度優先遍歷算法:

在深度優先遍歷中,需要兩個棧,這裏可以看出深度優先遍歷帶有遞歸的性質。一個棧用來輔助遍歷,即用來保存遍歷過程中裏面的頂點,另一個棧用來保存遍歷的順序。之所以另外需要一個棧來保存遍歷的順序的原因 與 廣度優先遍歷 中需要用另一個隊列來保存 遍歷順序 的原因相同。當深度優先遍歷到某個頂點時,若該頂點的所有鄰接點均已經被訪問,則發生回溯,即返回去遍歷 該頂點 的 前驅頂點 的 未被訪問的某個鄰接點。

深度優先遍歷的代碼與廣度優先遍歷的代碼很大的一個不同就是,在while 循環裏面,當取出棧頂/隊頭 頂點時,深度優先是用一個 if 語句 來執行邏輯,而廣度優先 則是用一個 while 循環來執行邏輯。

這是因爲:對於深度優先而言,訪問了 頂點A 時,緊接着只需要找到 頂點A 的一個未被訪問的鄰接點,再訪問該鄰接點即可。而對於廣度優先,訪問了 頂點A 時,就是要尋找 頂點A的所有未被訪問的鄰接點,再訪問 所有的這些鄰接點。

代碼對比如下:

while(!vertexStack.isEmpty()){
            VertexInterface<T> topVertex = vertexStack.peek();
            //找到該頂點的一個未被訪問的鄰接點,從該鄰接點出發又去遍歷鄰接點的鄰接點
            VertexInterface<T> nextNeighbor = topVertex.getUnvisitedNeighbor();
            if(nextNeighbor != null){
                nextNeighbor.visit();
                //由於用的是if,在這裏push鄰接點後,下一次while循環pop的是該鄰接點,然後又獲得它的鄰接點,---DFS
                vertexStack.push(nextNeighbor);
                traversalOrder.offer(nextNeighbor.getLabel());
            }
            else
                vertexStack.pop();//當某頂點的所有鄰接點都被訪問了時,直接將該頂點pop,這樣下一次while pop 時就回溯到前一個頂點

while(!vertexQueue.isEmpty()){
            VertexInterface<T> frontVertex = vertexQueue.poll();//出隊列,poll()在隊列爲空時返回null
            Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
            while(neighbors.hasNext())//對於 每個頂點都遍歷了它的鄰接表,即遍歷了所有的邊,複雜度爲O(E)
            {
                VertexInterface<T> nextNeighbor = neighbors.next();
                if(!nextNeighbor.isVisited()){
                    nextNeighbor.visit();//廣度優先遍歷未訪問的頂點
                    traversalOrder.offer(nextNeighbor.getLabel());
                    vertexQueue.offer(nextNeighbor);//將該頂點的鄰接點入隊列
                }
            }//end inner while
        }//end outer while

整個深度優先遍歷算法代碼如下:

public Queue<T> getDepthFirstTraversal(T origin) {
        resetVertices();//先將所有的頂點初始化--時間複雜度爲O(V)
        LinkedList<VertexInterface<T>> vertexStack = new LinkedList<>();//輔助DFS遞歸遍歷
        Queue<T> traversalOrder = new LinkedList<>();//保存DFS遍歷順序
        
        VertexInterface<T> originVertex = vertices.get(origin);//根據起始頂點的標識獲得起始頂點
        originVertex.visit();//訪問起始頂點,起始頂點的出度不能爲0(只考慮多於一個頂點的連通圖),若爲0,它就沒有鄰接點了
        vertexStack.push(originVertex);//各個頂點的入棧順序就是DFS的遍歷順序
        traversalOrder.offer(originVertex.getLabel());//每當一個頂點入棧時,就將它入隊列,從而隊列保存了整個遍歷順序
        
        while(!vertexStack.isEmpty()){
            VertexInterface<T> topVertex = vertexStack.peek();
            //找到該頂點的一個未被訪問的鄰接點,從該鄰接點出發又去遍歷鄰接點的鄰接點
            VertexInterface<T> nextNeighbor = topVertex.getUnvisitedNeighbor();//判斷所有未被訪問的鄰接點,也即遍歷了所有的邊--複雜度O(E)
            if(nextNeighbor != null){
                nextNeighbor.visit();
                //由於用的是if,在這裏push鄰接點後,下一次while循環pop的是該鄰接點,然後又獲得它的鄰接點,---DFS
                vertexStack.push(nextNeighbor);
                traversalOrder.offer(nextNeighbor.getLabel());
            }
            else
                vertexStack.pop();//當某頂點的所有鄰接點都被訪問了時,直接將該頂點pop,這樣下一次while pop 時就回溯到前一個頂點
        }//end while
        return traversalOrder;
    }

深度優先遍歷的算法的時間複雜度:O(V+E)--遍歷之前,給每個頂點進行初始化時需要遍歷所有頂點V,在遍歷過程中需要判斷頂點的鄰接點是否被遍歷,也即遍歷該頂點的鄰接表,鄰接表代表的實質是邊,邊總數爲 E,故總的時間複雜度爲O(V+E);空間複雜度:O(V)--用了兩個輔助棧

 

④拓撲排序算法

 求圖的拓撲序列的思路就是:先找到圖中一個出度爲0的頂點,訪問該頂點並將之入棧。訪問了該頂點之後,相當於指向該頂點的所有的邊都已經被刪除了。然後,繼續在圖中尋找下一個出度爲0且未被訪問的頂點,直至圖中所有的頂點都已被訪問。尋找這樣的頂點的方法實現如下:

private VertexInterface<T> getNextTopologyOrder(){//最壞情況下複雜度爲O(V+E)
        VertexInterface<T> nextVertex = null;
        Iterator<VertexInterface<T>> iterator = vertices.values().iterator();//獲得圖的頂點的迭代器
        boolean found = false;
        while(!found && iterator.hasNext()){
            nextVertex = iterator.next();
            //尋找出度爲0且未被訪問的頂點
            if(nextVertex.isVisited() == false && nextVertex.getUnvisitedNeighbor() == null)
                found = true;
        }
        return nextVertex;
    }

圖的拓撲排序實現代碼如下:

public Stack<T> getTopologicalSort() {
        /**
         *相比於《算法導論》中的拓撲排序藉助了DFS複雜度爲O(V+E),該算法的時間複雜度較大
         *因爲算法導論中介紹的圖的數據結構與此處實現的圖的數據結構不同
         *此算法的最壞時間複雜度爲O(V*(V+E))==V * max{V,E}
        */
        resetVertices();//先將所有的頂點初始化
        
        Stack<T> vertexStack = new Stack<>();//存放已訪問的頂點的棧,該棧就是一個拓撲序列
        int numberOfVertices = vertices.size();//獲得圖中頂點的個數
        
        for(int counter = 1; counter <= numberOfVertices; counter++){
            VertexInterface<T> nextVertex = getNextTopologyOrder();//獲得一個未被訪問的且出度爲0的頂點
            if(nextVertex != null){
                nextVertex.visit();
                vertexStack.push(nextVertex.getLabel());//遍歷完成後,出棧就可以獲得圖的一個拓撲序列
            }
        }
        return vertexStack;
    }

此拓撲排序算法實現的最壞情況下時間複雜度爲:O(V*max(V,E));空間複雜度爲:O(V)--定義一個輔助棧來保存遍歷順序

 

4,總結

本文實現了有向無環圖及四個常用的圖的遍歷算法,在客戶程序中只需要 new 一個圖對象,然後就可以調用這些算法了。哈哈,以後可以用這個類來測試一些複雜的算法了。。。

在實現過程中讓我明白了,數據結構與算法是緊密相關的,算法實現的難易程序及好壞依賴於你所設計的數據結構。

 

整個數據結構的學習至此爲止告一段落了。在整個學習過程中,用JAVA語言把常用的數據結構數組、鏈表、棧、隊列、樹、詞典、圖都實現了一遍。感覺學到最多的是加深了對JAVA集合類庫的理解和基本算法的理解(樹的遍歷算法和圖的遍歷算法)。

 

整個圖的實現的JAVA完整代碼下載(僅供學習)




發佈了33 篇原創文章 · 獲贊 5 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章