數據結構 -- 簡單圖的實現與遍歷 (Java)


---恢復內容開始---

  作者版權所有,轉載請注明出處,多謝. http://www.cnblogs.com/Henvealf/p/5534071.html

  前些天因為某些原因,就試着回想一下圖(graph)的相關內容,沒想腦子里一片空白,只怪當初沒有好好聽這門課.然后就學習了一下,這里做個小總結. 

1.概念 

  簡單圖(simple graph):就是由一些頂點(V,vertice) 和 連接這些頂點的一些邊(E,edge)所組成的結構,並且每對頂點之間只能存在一條邊.所以通常會用G = (V,E)來表示一個簡單圖.簡單圖也被稱為無向圖(undirected graph).

  說到無向圖就一定有有向圖(directed graph),有向圖同樣也用G = (V,E)來表示,都明白兩種圖的區別是什么,就不多說了.不過在這里說一些表示邊時候的區別:

  簡單圖的邊使用{Vi,Vj}的方式表示,表示連接頂點i頂點j的邊.因為是簡單圖,所以有:

  •    {Vi,Vj} = {Vj,Vi,}    

  有向圖的邊使用(Vi,Vj)的方式表示.意思是當前邊的方向是從頂點i頂點j,所以很明顯有:

  •        (Vi,Vj) ≠ (Vj,Vi)   

 

2.圖的表示

  有很多方法可以表示一個圖,不過思想上大致都一樣.比如下面一個圖(本文之后都會用這個圖來作為例子):

圖1

  自己畫的,或許丑了點:)

  先看使用鄰接表(adjacency list)來表示該圖:

V0 V1   V2 
V1 V0   V3 
V2 V0   V3   V4
V3 V1   V2   V5   V6
V4 V2   V7
V5 V3   V6
V6 V3   V5
V7 V4

  仔細觀察就會發現,鄰接表是在首列按順序(正序倒序都可)列出所有頂點,然后第二行列出與頂點相鄰的所有頂點,例如第一行,圖中與V0相鄰的頂點有V1與V2.所以第二列的內容便是 V1 與 V2.

 

  另一種表示方法是使用鄰接矩陣(adjacency matrix),思想與鄰接表大致相同,不同的是鄰接表是將與某一頂點相鄰的頂點們列出,而鄰接矩陣是將他們標注出。顧名思義就是使用矩陣表示,如在本例中,一共有8個頂點,所以此時鄰接矩陣的大小就為8*8。如下:

  V0 V1 V2 V3 V4 V5 V6 V7
V0 oo 1 1 oo oo oo oo oo
V1 oo oo oo 1 oo oo oo oo
V2 oo oo oo 1 1 oo oo oo
V3 oo 1 1 oo oo 1 1 oo
V4 oo oo 1 oo oo oo oo 1
V5 oo oo oo 1 oo oo 1 oo
V6 oo oo oo 1 oo 1 oo oo
V7 oo oo oo oo 1 oo oo oo

  觀察可以發現,如果頂點i與頂點j之間存在邊,就將矩陣的aij項設為1,其他情況就設為無窮大(oo)。公式就是:

                      {   1    邊(Vi ,Vj)存在 

                    aij = {

                      {   無窮大    其他情況

  如果對於有向圖,此公式就得改為:

                      {   1    邊{Vi ,Vj}存在 

                    aij = {

                      {   無窮大    其他情況

  

  對於使用哪種表示法,這要取決與你所要處理的問題,如果你僅僅只是處理與某一頂點鄰接的頂點,例如遍歷,很明顯使用表比使用矩陣所要步數要少很多。當你需要對圖進行插入或者刪除頂點的操作,就應該使用鄰接矩陣,你只需要將矩陣上的數從0變為1,或者從1變為0即可。而使用鄰接表,你還得要對表進行維護。

  本例就選用鄰接矩陣來表示圖1,用二維數組就能直接實現,無窮大被換成了名為oo的變量.如下:

 1         int oo = Integer.MAX_VALUE;
 2         int[][] racs1 = new int[][]{
 3                 {oo, 1, 1,oo,oo,oo,oo,oo},
 4                 { 1,oo,oo, 1,oo,oo,oo,oo},
 5                 { 1,oo,oo, 1, 1,oo,oo,oo},
 6                 {oo, 1, 1,oo,oo, 1, 1,oo},
 7                 {oo,oo, 1,oo,oo,oo,oo, 1},
 8                 {oo,oo,oo, 1,oo,oo, 1,oo},
 9                 {oo,oo,oo, 1,oo, 1,oo,oo},
10                 {oo,oo,oo,oo, 1,oo,oo,oo},
11         };

    

  除此之外,這里還設定義了一個數組,用於為每個頂點添加一些信息.也就是各個頂點的名字.如果對與具體的問題,例如每個頂點代表地圖上的一個城市,可使用像"北京","上海"城市名.當然可以很據特定情況將頂點的相關內容封裝一下.

1         String[] verticeInfos1 = new String[] {
2                 "V0","V1","V2","V3","V4","V5","V6","V7"
3         };

 

  另:有一種圖中有孤立頂點的情況,假設上面的圖中有還有一個頂點V8,但它不與任何其他頂點相鄰,則該怎么用表或者矩陣來表示那?就當留給大家的一個小問題。

3.圖的遍歷

  3.1 先寫一個名為Graph的類.用這個類來封裝圖的相關字段和遍歷方法.先看類的字段與構造方法:

 1 /**
 2  * 使用鄰接矩陣實現圖<p>
 3  * 深度優先遍歷與廣度優先遍歷<p>
 4  * 求最短路徑:<p>
 5  *      1. Dijkstra 算法 <p>
 6  *      2. Ford 算法 <p>
 7  *      3. 通用型的糾正標記算法<p>
 8  * Created by Henvealf on 16-5-22.
 9  */
10 public class Graph<T> {
11     private int[][] racs;       //鄰接矩陣
12     private T[] verticeInfo;   //各個點所攜帶的信息.
13 
14     private int verticeNum;             //節點的數目,
15     private int[] visitedCount;         //記錄訪問
16     private int[] currDist;             //最短路徑算法中用來記錄每個節點的當前路徑長度.
17 
18     public Graph(int[][] racs, T[] verticeInfo){
19         if(racs.length != racs[0].length){
20             throw new IllegalArgumentException("racs is not a adjacency matrix!");
21         }
22         if(racs.length != verticeInfo.length ){
23             throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!");
24         }
25         this.racs = racs;
26         this.verticeInfo = verticeInfo;
27         verticeNum = racs.length;
28         visitedCount = new int[verticeNum];
29     }
30     //.....其他方法
31 }

 

  這里使用了模板來模板化 verticeInfos.

  還要說明的便是數組 int[] visitedCount; 其作用是為了標記圖中的頂點是否被訪問過. visitedCount[i] == 0 就說明頂點 i 還未被訪問過,所以在進行遍歷操作前需要初始化該數組全為0.方法如下:  

1     /**
2      * 將記錄訪問的數組初始化為0
3      */
4     private void initVisitedCount(){
5         for(int i = 0; i < visitedCount.length; i ++){  
6             visitedCount[i] = 0;
7         }
8     }

 

  3.2 圖的遍歷和樹的遍歷類似,分為深度優先遍歷與廣度優先遍歷.

  深度優先遍歷,簡單說就是先沿着一條路線最靠左或最靠右的路線往下走,並把路過的頂點標記為已訪問.一直走到無路可走,或者下一站的頂點都被已經訪問過了,就順着剛才走過的路往回看(回溯),當發現回溯中的某一頂點還有其他未被訪問過的分支的時候,就選擇這個分支繼續,重復上面的過程繼續遍歷.直到回溯到了遍歷的出發點,就結束遍歷.但如果檢查發現圖中還存在未被訪問過的頂點,就說明圖中還存在孤立與本圖的分圖.此時則任意選擇一個未被訪問過的頂點繼續訪問.  最后當圖中所有的頂點都被訪問過的時候,就說明遍歷完成. 

  所以這里需要一個方法,用來判斷圖中頂點的訪問情況:

 1     /**
 2      * 尋找沒有被訪問過的頂點.
 3      * @return > 0 即為還未被訪問過的頂點.   -1 說明所有的節點都被訪問過了.
 4      */
 5     private int findNotVisited(){
 6         for(int i = 0; i < noteNum; i ++){
 7             if(visitedCount[i] == 0){
 8                 return i;
 9             }
10         }
11         return -1;
12     }

 

  要尋找與頂點的相鄰節點時,我們可以發現簡單圖的鄰接矩陣以對角線對稱,所以尋找相鄰頂點的時候只需要遍歷一半就可以,大大的提高了遍歷的效率,不過對於有向圖就需要遍歷全圖.這里只討論簡單圖,有向圖自行修改區分便可,這里是遍歷全圖:

 1     /**
 2      * 深度遍歷的遞歸
 3      * @param begin  從第幾個節點開始遍歷
 4      */
 5     public void DFS(int begin, Queue<T> edges){
 6         visitedCount[begin] = 1;                         //標記begin為已訪問
 7         edges.offer(verticeInfo[begin]);                 //加入記錄隊列
 8         for(int a = 0; a < verticeNum; a++){             //遍歷相鄰的點
 9             if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){   //相鄰的點未被訪問過
10                 DFS(a,edges);
11             }
12         }
13     }
14 
15     /**
16      * 開始深度優先遍歷
17      * @return  返回保持有遍歷之后的順序的隊列
18      */
19     public Queue<T> depthFirstSearch(){
20         initVisitedCount();         //將記錄訪問次序的數組初始化為0
21         Queue<T> edges = new LinkedList<>();    //用於存儲遍歷過的點,用於輸出
22         int begin = -1;
23         while((begin = findNotVisited()) != -1){        //不等於-1說明還有未訪問過的點
24             DFS(begin,edges);
25         }
26         return edges;
27     }

 

   廣度優先遍歷.與樹的廣度優先遍歷相似,就是逐層遍歷.代碼如下:

 1     /**
 2      * 廣度優先遍歷
 3      * @return  返回保持有遍歷之后的順序的隊列
 4      */
 5     public Queue<T> breadthFirstSearch(){
 6         initVisitedCount();                          //將記錄訪問次序的數組初始化為0
 7         Queue<Integer> tallyQueue = new LinkedList<>();             //初始化隊列
 8         Queue<T> edges = new LinkedList<>();    //用於存儲遍歷過的點,用於輸出
 9         int nowVertice = -1;                         //當前所在的點
10         while((nowVertice = findNotVisited()) != -1){   //尋找還未被訪過問的點
11             visitedCount[nowVertice] = 1;             //設置訪問標記
12             edges.offer(verticeInfo[nowVertice]);
13             tallyQueue.offer(nowVertice);               //將當前孤立部分一個頂點加入記錄隊列中
14             while(!tallyQueue.isEmpty()){                           //只要隊列不為空
15                 nowVertice = tallyQueue.poll();                     //取出隊首的節點
16                 for(int a = 0; a < verticeNum; a++){     //遍歷所有和nowVertice相鄰的節點
17                     if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) {                      //沒有訪問過
18                         visitedCount[a] = 1;                        //記為標記過
19                         tallyQueue.offer(a);                        //加入隊列,上面會繼續取出.來遍歷
20                         edges.offer(verticeInfo[a]);                      //記錄
21                     }
22                 }
23             }
24         }
25         return edges;
26     }

 

 

 

 

  這里需要兩個隊列,一個隊列用於存儲遍歷過程,另一個用於保存第n層被遍歷的順序,等到遍歷第n+1層的時候,取出的頂點就是按照上層的順序來排列,也就能按照上層的順序來繼續遍歷.

 

下面是Graph完整代碼:

 

  1 package com.henvealf.datastructures.graph.arcs;
  2 
  3 import java.util.*;
  4 
  5 /**
  6  * 使用鄰接矩陣實現圖<p>
  7  * 深度優先遍歷與廣度優先遍歷<p>
  8  * 求最短路徑:<p>
  9  *      1. Dijkstra 算法 <p>
 10  *      2. Ford 算法 <p>
 11  *      3. 通用型的糾正標記算法<p>
 12  * Created by Henvealf on 16-5-22.
 13  */
 14 public class Graph<T> {
 15     private int[][] racs;       //鄰接矩陣
 16     private T[] verticeInfo;   //各個點所攜帶的信息.
 17 
 18     private int verticeNum;             //節點的數目,
 19     private int[] visitedCount;         //記錄訪問
 20     private int[] currDist;             //最短路徑算法中用來記錄每個節點的當前路徑長度.
 21 
 22     public Graph(int[][] racs, T[] verticeInfo){
 23         if(racs.length != racs[0].length){
 24             throw new IllegalArgumentException("racs is not a adjacency matrix!");
 25         }
 26         if(racs.length != verticeInfo.length ){
 27             throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!");
 28         }
 29         this.racs = racs;
 30         this.verticeInfo = verticeInfo;
 31         verticeNum = racs.length;
 32         visitedCount = new int[verticeNum];
 33     }
 34 
 35     /**
 36      * 深度遍歷的遞歸
 37      * @param begin  從第幾個節點開始遍歷
 38      */
 39     public void DFS(int begin, Queue<T> edges){
 40         visitedCount[begin] = 1;                         //標記begin為已訪問
 41         edges.offer(verticeInfo[begin]);                 //加入記錄隊列
 42         for(int a = 0; a < verticeNum; a++){             //遍歷相鄰的點
 43             if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){   //相鄰的點未被訪問過
 44                 DFS(a,edges);
 45             }
 46         }
 47     }
 48 
 49     /**
 50      * 開始深度優先遍歷
 51      * @return  返回保持有遍歷之后的順序的隊列
 52      */
 53     public Queue<T> depthFirstSearch(){
 54         initVisitedCount();         //將記錄訪問次序的數組初始化為0
 55         Queue<T> edges = new LinkedList<>();    //用於存儲遍歷過的點,用於輸出
 56         int begin = -1;
 57         while((begin = findNotVisited()) != -1){        //不等於-1說明還有未訪問過的點
 58             DFS(begin,edges);
 59         }
 60         return edges;
 61     }
 62 
 63     /**
 64      * 廣度優先遍歷
 65      * @return  返回保持有遍歷之后的順序的隊列
 66      */
 67     public Queue<T> breadthFirstSearch(){
 68         initVisitedCount();                          //將記錄訪問次序的數組初始化為0
 69         Queue<Integer> tallyQueue = new LinkedList<>();             //初始化隊列
 70         Queue<T> edges = new LinkedList<>();    //用於存儲遍歷過的點,用於輸出
 71         int nowVertice = -1;                         //當前所在的點
 72         while((nowVertice = findNotVisited()) != -1){   //尋找還未被訪過問的點
 73             visitedCount[nowVertice] = 1;             //設置訪問標記
 74             edges.offer(verticeInfo[nowVertice]);
 75             tallyQueue.offer(nowVertice);               //將當前孤立部分一個頂點加入記錄隊列中
 76             while(!tallyQueue.isEmpty()){                           //只要隊列不為空
 77                 nowVertice = tallyQueue.poll();                     //取出隊首的節點
 78                 for(int a = 0; a < verticeNum; a++){     //遍歷所有和nowVertice相鄰的節點
 79                     if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) {                      //沒有訪問過
 80                         visitedCount[a] = 1;                        //記為標記過
 81                         tallyQueue.offer(a);                        //加入隊列,上面會繼續取出.來遍歷
 82                         edges.offer(verticeInfo[a]);                      //記錄
 83                     }
 84                 }
 85             }
 86         }
 87         return edges;
 88     }
 89 
 90     /**
 91      * 尋找沒有被訪問過的頂點.
 92      * @return > 0 即為還未被訪問過的頂點.   -1 說明所有的節點都被訪問過了.
 93      */
 94     private int findNotVisited(){
 95         for(int i = 0; i < verticeNum; i ++){
 96             if(visitedCount[i] == 0){
 97                 return i;
 98             }
 99         }
100         return -1;
101     }
102 
103     /**
104      * 將記錄訪問的數組初始化為0
105      */
106     private void initVisitedCount(){
107         for(int i = 0; i < visitedCount.length; i ++){
108             visitedCount[i] = 0;
109         }
110     }
111 }

 

 

 

 

下面測試類:

package com.henvealf.datastructures.graph.arcs;

import java.util.Queue;

/**
 * 圖的測試類
 * Created by Henvealf on 16-5-22.
 */
public class Main {

    public static void main(String[] args) {

        int[][] racs = new int[][]{
                {0,1,0,1,0,},
                {1,0,1,0,1,},
                {0,1,0,1,1,},
                {1,0,1,0,0,},
                {0,1,1,0,0,},
        };
        int oo = Integer.MAX_VALUE;
        int[][] racs1 = new int[][]{
                {oo, 1, 1,oo,oo,oo,oo,oo},
                { 1,oo,oo, 1,oo,oo,oo,oo},
                { 1,oo,oo, 1, 1,oo,oo,oo},
                {oo, 1, 1,oo,oo, 1, 1,oo},
                {oo,oo, 1,oo,oo,oo,oo, 1},
                {oo,oo,oo, 1,oo,oo, 1,oo},
                {oo,oo,oo, 1,oo, 1,oo,oo},
                {oo,oo,oo,oo, 1,oo,oo,oo},
        };

        String[] verticeInfos1 = new String[] {
                "V0","V1","V2","V3","V4","V5","V6","V7"
        };
        Graph<String> graph = new Graph<>(racs2,verticeInfos2);
        Queue<String> dr = graph.depthFirstSearch();
        Queue<String> br = graph.breadthFirstSearch();

        System.out.println("--遍歷");
        System.out.println("----深度優先結果: " + dr);
        System.out.println("----廣度優先結果: " + br);
}

 

不足之處,請多多指出,感激不盡.

End...   By Henvealf/自安 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM