數據結構與算法(十一):圖的基礎以及遍歷代碼實現


本篇目錄

一、圖定義

圖是一種較線性表和樹更為復雜的數據結構,其定義為:

圖是由頂點的有窮非空集合與頂點之間邊的集合構成,通常表示為:G(V, E), G表示一個圖,V表示圖中頂點的集合,E表示頂點之間邊的集合。

如下,就是一個圖: 

二、圖術語了解

圖中數據元素我們稱之為頂點,圖中任意兩個頂點都可能存在關系,頂點之間關系用邊來表示。

若兩個頂點Vi與Vj之間的邊沒有方向,則稱這條邊為無向邊, 用(Vi,Vj)表示,注意這里是圓括號

如果圖中任意頂點之間都是無向邊,則稱該圖為無向圖,在無向圖中任意兩個頂點之間都存在邊,則稱該圖為無向完全圖,上圖就是一個無向完全圖。

若兩個頂點Vi與Vj之間的邊有方向,則稱這條邊為有向邊,也稱作弧, 用<vi,Vj>表示,注意這里是尖括號</v

如果圖中任意頂點之間都是有向邊,則稱該圖為有向圖,在有向圖中任意兩個頂點之間都存在方向互為相反的兩條弧,則稱該圖為有向完全圖,如下圖:

有些圖的邊或者弧具有與其相關的數字,這種與圖或弧相關的數字叫做權值

在無向圖中如果兩個頂點之間有路徑,則稱這兩個頂點是聯通的,如果圖中任意兩個頂點都是聯通的,則稱圖是連通圖

在無向圖中頂點的邊數叫做頂點的。 
在有向圖中頂點分為出度入度,入度就是指向自己的邊數,而出度則相反。

如下圖: 

A結點:出度為3,入度為1 
B結點:出度為1,入度為2

以上講解了一些圖的基本術語,沒什么難度,大概了解一下就可以了。

三、圖的存儲

圖的結構比較復雜,任意兩個元素都可能產生關系,所以一般存儲結構無法滿足。

鄰接矩陣存儲方式

圖是由頂點和邊(或者弧)組成的,可以單獨存儲頂點和邊,頂點簡單,可以用一位數組來存儲,但是邊呢?邊就是兩個頂點之間的關系,可以用一個二維數組來存儲,這種存儲方式就是鄰接矩陣存儲方式

鄰接矩陣存儲方式就是用兩個數組來存儲,一個一維數組,一個二維數組,一維數組用來存儲頂點,二維數組用來存儲頂點之間的關系,也就是邊或者弧。

無向圖鄰接矩陣存儲方式:

如下無向圖: 

一維數組存儲頂點數組:

二維數組存儲頂點(邊,弧)之間關系:

無向圖邊之間關系的二維數組用0或者1來填充,如果兩個頂點之間直接連通則為1,不直接連通則為0,有個注意點就是直接不是間接連通。比如A與B之間直接連通所以填1,而A與D之間不直接連通則為0.

有向圖鄰接矩陣存儲方式:

如下有向圖:

存儲頂點的一維數組與上面一樣

二維數組存儲頂點(邊,弧)之間關系:

無向圖中兩個頂點之間有直接相連的邊則為1,而有向圖中如上圖中B與C之間,有B指向C的邊則為1,而沒有C指向B的邊則為0。

帶權鄰接矩陣存儲方式:

如下圖有向帶權圖: 

存儲頂點的一維數組與上面一樣

二維數組存儲頂點(邊,弧)之間關系:

很簡單,就是將有向圖中1替換為對應權值,此外,如圖中A,C之間,沒有A指向C的邊則用∞表示。

對於帶權有向圖將各個頂點理解成一個個村庄,邊就是村庄之間的路,權值就是距離,AB村庄之間有A指向B的路程,並且路程是100,則二維數組中直接用100表示,BA之間沒有路,則用無窮大表示,代表永遠不可達,自己到自己就是0了。

好了,以上介紹了圖的一些術語以及其中一種存儲方式,另一種方式為鄰接表的存儲方式,這里就不提了,有興趣可以自己去找找資料了解一下。

四、java實現圖的常用方法

在上面我們了解了鄰接矩陣的方式存儲圖,圖大部分信息已經展示在二維數組里面了,很多方法也都是從二維數組中提取信息,接下來我們用java具體實現圖以及一些核心方法。

圖的創建

圖的創建就是外部輸入圖的信息我們來存儲下來就可以了,代碼如下:

 1    private  int[] vertices;// 存儲圖的頂點
 2    public int[][] matrix; // 存儲圖的邊
 3    private int verticeSize; // 頂點的數量
 4    //帶權有向圖中代表相鄰兩個頂點之間不可達
 5    private static final int MAX_WEIGHT = 0xFFFF;
 6
 7    public Graph(int verticeSize) {
 8        this.verticeSize = verticeSize;
 9        vertices = new int[verticeSize];
10        matrix = new int[verticeSize][verticeSize];
11        //初始化存儲頂點的一維數組
12        for(int i = 0; i < verticeSize; i++) {
13            vertices[i] = i;
14        }
15    }
獲取圖中兩個頂點的權值

獲取圖中兩個頂點的權值其實就是二維數組中對應位置的值:

 1    /**
 2     * 計算V1到v2 的權值
 3     * @param v1
 4     * @param v2
 5     * @return
 6     */
 7    public int getWidget(int v1, int v2) {
 8        int weight = matrix[v1][v2];
 9        return weight == 0 ? 0 : (weight == MAX_WEIGHT ? -1:weight);
10    }
獲取圖某個頂點V的出度

頂點V的出度的獲取只需要遍歷其所在二維數組那一行數據有幾個有效權值即可:

 1    /**
 2     * 計算某個頂點V的出度
 3     * @param v
 4     * @return
 5     */
 6    public int getOutDegree(int v) {
 7        int count = 0;
 8        for(int i = 0; i < verticeSize; i++ ) {
 9            //遍歷其所在二維數組那一行的數據
10            if (matrix[v][i] != 0 && matrix[v][i] != MAX_WEIGHT) {
11                count ++;
12            }
13        }
14        return count;
15    }
獲取圖某個頂點V的入度

出度是遍歷其所在行,入度就是遍歷其所在列了(這里不明白可以自己想想):

 1    /**
 2     *  計算某個頂點V的入度
 3     * @param v
 4     * @return
 5     */
 6    public int getInDegree(int v) {
 7        int count = 0;
 8        for(int i = 0; i < verticeSize; i++) {
 9            if (matrix[i][v] != 0 && matrix[i][v] != MAX_WEIGHT) {
10                count ++;
11            }
12        }
13        return count;
14    }
獲取某個頂點的第一個鄰接點

第一個鄰接點就是頂點第一個可以直接到達的點,比如頂點A有直接指向B,C,D的邊,而A第一個指向B,其次C,D,則B是A的第一個鄰接點,查找鄰接點也很簡單,就是頂點所在二維數組的那一行數據第一個有效權值的角標:

 1    /**
 2     * 獲取某個頂點的第一個鄰接點
 3     * @param v
 4     * @return
 5     */
 6    public int getFirstNeightbor(int v) {
 7        for(int i = 0; i < verticeSize; i++) {
 8            if (matrix[v][i] > 0 && matrix[v][i] != MAX_WEIGHT) {
 9                return i;
10            }
11        }
12        return -1;
13    }
查找節點v ,index開始的下一個鄰接點

上面是從0查找節點V的第一個鄰接點,這里是從index角標開始查找其第一個鄰接點:

 1    /**
 2     * 查找節點v ,index開始的下一個鄰接點
 3     * @param v 節點
 4     * @param index  開始角標
 5     * @return
 6     */
 7    public int getNextNeightBor(int v,int index) {
 8        for(int j = index +1; j < verticeSize; j++) {
 9            if (matrix[v][j] > 0 && matrix[v][j] != MAX_WEIGHT) {
10                return j;
11            }
12        }
13        return -1;
14    }

以上了解了一些圖的常用方法,接下來我們輸入一個圖測試一下。
圖如下:


我們需要自己提取出圖中各頂點之間的關系也就是二維數組:

然后我們就可以測試了:

 1        Graph graph = new Graph(4);//創建圖
 2        //輸入圖的邊關系二維數組
 3        int[] a0 = new int[]{0, 8, 4, 6};
 4        int[] a1 = new int[]{Graph.MAX_WEIGHT, 0, Graph.MAX_WEIGHT, 7};
 5        int[] a2 = new int[]{Graph.MAX_WEIGHT, Graph.MAX_WEIGHT, 0, Graph.MAX_WEIGHT};
 6        int[] a3 = new int[]{Graph.MAX_WEIGHT, Graph.MAX_WEIGHT, 5, 0};
 7        graph.matrix[0] = a0;
 8        graph.matrix[1] = a1;
 9        graph.matrix[2] = a2;
10        graph.matrix[3] = a3;
11        //以下求結點0的信息,圖內部一維數組存儲的並不是A,B,C,D 而是0,1,2,3 一一對應
12        //這里也就是求節點A的入度,出度,第一個鄰接點,C節點以后的第一個鄰接點
13        System.out.println(graph.getInDegree(0));
14        System.out.println(graph.getOutDegree(0));
15        System.out.println(graph.getFirstNeightbor(0));
16        System.out.println(graph.getNextNeightBor(0, 2));

打印信息如下:

0  //A節點入度為0
3  //A節點出度為3
1  //A節點第一個鄰接點為1,代表B
3  //A節點C節點之后的第一個鄰接點為3,代表D結點

好了,以上講解一下圖的基礎概念以及代碼實現一些重要方法,都比較簡單,沒有過多分析,接下來我們了解一下圖的遍歷方式。

五、圖的遍歷

和樹的遍歷類似,我們希望從圖中某一頂點出發訪遍圖中其余頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷

圖的遍歷主要分為深度優先廣度優先遍歷,它們對無向圖和有向圖都適用。

深度優先遍歷

大體思路:初始狀態圖中所有頂點都未曾被訪問,則深度優先遍歷可以從圖中某一頂點V出發,先訪問此頂點,然后依次從V的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中和V有路徑相通的頂點都被訪問到;若此時圖中仍未有頂點沒被訪問到,則另選圖中另一個未被訪問的頂點作起始點,重復上述操作,直至圖中所有頂點都被訪問到。

如下圖:

進行深度優先遍歷:
先訪問A,A有鄰接點B,那么訪問B,B有鄰接點D,則訪問D,D有鄰接點C,則繼續訪問C。

所以上圖深度優先遍歷為:

深度優先代碼實現:

isVisited為一個數組,記錄節點是否被訪問過

 1    /**
 2     * 深度優先
 3     */
 4    public void dfs() {
 5        //對每一個節點進行一次深度優先遍歷
 6        for (int i = 0; i < verticeSize; i++) {
 7            if (!isVisited[i]) {//當前結點沒有被訪問過
 8                System.out.println("viested vertice " + i);
 9                dfs(i);//從當前節點開始進行一次深度優先遍歷
10            }
11        }
12    }
13
14    //從節點i開始進行深度優先遍歷
15    public void dfs(int i) {
16        //節點設置為已經被訪問
17        isVisited[i] = true;
18        //獲取第一個鄰接點
19        int v = getFirstNeightbor(i);
20        //有鄰接點
21        while (v != -1) {
22            //並且沒有被訪問過
23            if (!isVisited[v]) {
24                //訪問節點
25                System.out.println("visted vertice " + v);
26                //從當前節點開始繼續深度遍歷
27                dfs(v);
28            }
29            //上一個鄰接點已經被訪問則繼續查找下一個鄰接點
30            v = getNextNeightBor(i, v);
31        }
32    }
廣度優先遍歷

大體思路:假設從圖中V出發,在訪問了V之后依次訪問V的各個未曾被訪問過的鄰接點,然后分別從這些鄰接點出發依次訪問它們的鄰接點,並使“先被訪問的頂點的鄰接點”先與“后被訪問的頂點的鄰接點”被訪問,直至圖中所有已被訪問的頂點的鄰接點都被訪問到,若此時圖中尚有頂點未被訪問,則另選圖中一個未被訪問的頂點作起始點,重復上述過程,直至圖中所有頂點都被訪問到為止。

如下圖:

進行廣度優先遍歷;
先訪問節點A,A有鄰接點B, C,則依次訪問B, C,然后訪問B的鄰接點(B先於C被訪問,所以先訪問B的鄰接點)D,在訪問C的鄰接點,此時所有結點都已經被訪問了,所以遍歷結束。

所以上圖廣度優先遍歷為:

廣度優先遍歷代碼實現:

 1    /**
 2     * 廣度優先
 3     */
 4    public void bfs(){
 5        for (int i = 0; i < verticeSize; i++) {
 6            isVisited[i]=false;
 7        }
 8        for (int i = 0; i < verticeSize; i++) {
 9            if(!isVisited[i]){
10                isVisited[i]=true;
11                System.out.println("visited vertice:"+ i);
12                bfs(i);
13            }
14        }
15    }
16
17    public void bfs(int i) {
18        //當做隊列使用,先訪問的先放入隊列
19        LinkedList<Integer> queue = new LinkedList<>();
20        //找第一個鄰接點
21        int fn = getFirstNeightbor(i);
22        if (fn == -1) {
23            return;
24        }
25        //沒有被訪問過
26        if (!isVisited[fn]) {
27            isVisited[fn] = true;//設置為被訪問
28            System.out.println("visted vertice:" + fn);
29            queue.offer(fn);
30        }
31        //訪問各個未曾訪問過的鄰接點
32        int next = getNextNeightBor(i, fn);
33        while (next != -1) {
34            if (!isVisited[next]) {
35                isVisited[next] = true;
36                System.out.println("visted vertice:" + next);
37                queue.offer(next);
38            }
39            next = getNextNeightBor(i, next);
40        }
41        //從隊列中取出來一個,重復之前的操作
42        while(!queue.isEmpty()){
43            int point=queue.poll();
44            bfs(point);
45        }
46    }

好了,以上就是圖的遍歷方式,介紹到此。
關於圖部分,本系列就介紹到此,主要就介紹了一些基礎概念,重要方法以及兩種遍歷方式,當然圖部分還有最小生成樹算法以及最短路徑算法,感興趣的同學可以自己去查找學習一下。

好了,本篇到此為止。

最后附上整個類的代碼:

 1 public class Graph {
  2    private  int[] vertices;// 存儲圖的頂點
  3    public int[][] matrix; // 存儲圖的邊
  4    private int verticeSize; // 頂點的數量
  5    //帶權有向圖中代表相鄰兩個頂點之間不可達
  6    public static final int MAX_WEIGHT = 0xFFFF;
  7    //記錄節點是否被訪問過
  8    private boolean[] isVisited;
  9
 10    public Graph(int verticeSize) {
 11        this.verticeSize = verticeSize;
 12        vertices = new int[verticeSize];
 13        matrix = new int[verticeSize][verticeSize];
 14        isVisited = new boolean[verticeSize];
 15        for(int i = 0; i < verticeSize; i++) {
 16            vertices[i] = i;
 17        }
 18    }
 19
 20    /**
 21     * 計算V1到v2 的權值
 22     * @param v1
 23     * @param v2
 24     * @return
 25     */
 26    public int getWidget(int v1, int v2) {
 27        int weight = matrix[v1][v2];
 28        return weight == 0 ? 0 : (weight == MAX_WEIGHT ? -1:weight);
 29    }
 30
 31    /**
 32     * 獲取所有的頂點
 33     * @return
 34     */
 35    public int[] getVertices() {
 36        return vertices;
 37    }
 38
 39    /**
 40     * 計算某個頂點V的出度
 41     * @param v
 42     * @return
 43     */
 44    public int getOutDegree(int v) {
 45        int count = 0;
 46        for(int i = 0; i < verticeSize; i++ ) {
 47            if (matrix[v][i] != 0 && matrix[v][i] != MAX_WEIGHT) {
 48                count ++;
 49            }
 50        }
 51        return count;
 52    }
 53
 54    /**
 55     *  計算某個頂點V的入度
 56     * @param v
 57     * @return
 58     */
 59    public int getInDegree(int v) {
 60        int count = 0;
 61        for(int i = 0; i < verticeSize; i++) {
 62            if (matrix[i][v] != 0 && matrix[i][v] != MAX_WEIGHT) {
 63                count ++;
 64            }
 65        }
 66        return count;
 67    }
 68
 69    /**
 70     * 獲取某個頂點的第一個鄰接點
 71     * @param v
 72     * @return
 73     */
 74    public int getFirstNeightbor(int v) {
 75        for(int i = 0; i < verticeSize; i++) {
 76            if (matrix[v][i] > 0 && matrix[v][i] != MAX_WEIGHT) {
 77                return i;
 78            }
 79        }
 80        return -1;
 81    }
 82
 83    /**
 84     * 查找節點v ,index開始的下一個鄰接點
 85     * @param v 節點
 86     * @param index  節點
 87     * @return
 88     */
 89    public int getNextNeightBor(int v,int index) {
 90        for(int j = index +1; j < verticeSize; j++) {
 91            if (matrix[v][j] > 0 && matrix[v][j] != MAX_WEIGHT) {
 92                return j;
 93            }
 94        }
 95        return -1;
 96    }
 97
 98
 99    /**
100     * 深度優先
101     */
102    public void dfs() {
103        //對每一個節點進行一次深度優先遍歷
104        for (int i = 0; i < verticeSize; i++) {
105            if (!isVisited[i]) {//當前結點沒有被訪問過
106                System.out.println("viested vertice " + i);
107                dfs(i);//從當前節點開始進行一次深度優先遍歷
108            }
109        }
110    }
111
112    //從節點i開始進行深度優先遍歷
113    public void dfs(int i) {
114        //節點設置為已經被訪問
115        isVisited[i] = true;
116        //獲取第一個鄰接點
117        int v = getFirstNeightbor(i);
118        //有鄰接點
119        while (v != -1) {
120            //並且沒有被訪問過
121            if (!isVisited[v]) {
122                //訪問節點
123                System.out.println("visted vertice " + v);
124                //從當前節點開始繼續深度遍歷
125                dfs(v);
126            }
127            //上一個鄰接點已經被訪問則繼續查找下一個鄰接點
128            v = getNextNeightBor(i, v);
129        }
130    }
131
132    /**
133     * 廣度優先
134     */
135    public void bfs(){
136        for (int i = 0; i < verticeSize; i++) {
137            isVisited[i]=false;
138        }
139        for (int i = 0; i < verticeSize; i++) {
140            if(!isVisited[i]){
141                isVisited[i]=true;
142                System.out.println("visited vertice:"+ i);
143                bfs(i);
144            }
145        }
146    }
147
148    public void bfs(int i) {
149        //當做隊列使用,先訪問的先放入隊列
150        LinkedList<Integer> queue = new LinkedList<>();
151        //找第一個鄰接點
152        int fn = getFirstNeightbor(i);
153        if (fn == -1) {
154            return;
155        }
156        //沒有被訪問過
157        if (!isVisited[fn]) {
158            isVisited[fn] = true;//設置為被訪問
159            System.out.println("visted vertice:" + fn);
160            queue.offer(fn);
161        }
162        //訪問各個未曾訪問過的鄰接點
163        int next = getNextNeightBor(i, fn);
164        while (next != -1) {
165            if (!isVisited[next]) {
166                isVisited[next] = true;
167                System.out.println("visted vertice:" + next);
168                queue.offer(next);
169            }
170            next = getNextNeightBor(i, next);
171        }
172        //從隊列中取出來一個,重復之前的操作
173        while(!queue.isEmpty()){
174            int point=queue.poll();
175            bfs(point);
176        }
177    }
178}


免責聲明!

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



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