資料結構的Java實現(十四)—— 圖
1、圖的定義
圖通常有個固定的形狀,這是由物理或抽象的問題所決定的。比如圖中節點表示城市,而邊可能表示城市間的班機航線。如下圖是美國加利福利亞簡化的高速公路網:
①、鄰接:如果兩個頂點被同一條邊連線,就稱這兩個頂點是鄰接的。如上圖 I 和 G 就是鄰接的,而 I 和 F 就不是。
②、路徑:從某頂點到另一頂點經過的邊的序列。比如從頂點B到頂點J的路徑為 BAEJ。
③、連通圖和非連通圖:至少有一條路徑可以連線起所有的頂點,則稱為連通圖;反之為非連通圖。
④、有向圖和無向圖:如果邊是有方向的,可以從任意一邊到達另一邊,則稱為有向圖;反之為無向圖。
⑤、有權圖和無權圖:邊被賦予一個權值,能代表兩個頂點間的物理距離,這種圖被稱為有權圖;反之為無權圖。
2、在程式中表示圖
①、頂點:
在大多數情況下,頂點表示某個真實世界的物件,這個物件必須用資料項來描述。通常用一個頂點類的物件來表示一個頂點,這裡我們僅僅在頂點中儲存了一個字母來標識頂點,同時還有一個標誌位,用來判斷該頂點有沒有被訪問過(用於後面的搜尋)。
package yrwan13; // 頂點類 public class Vertex { public char label; public boolean wasVisited; public Vertex(char label) { this.label = label; wasVisited = false; } }
②、邊:
圖沒有固定的結構,圖的每個頂點可以與任意多個頂點相連,為了模擬這種自由形式的組織結構,用如下兩種方式表示圖:鄰接矩陣和鄰接表(如果一條邊連線兩個頂點,那麼這兩個頂點就是鄰接的)。
鄰接矩陣:
鄰接矩陣是一個二維陣列,資料項表示兩點間是否存在邊,如果圖中有 N 個頂點,鄰接矩陣就是 N*N 的陣列。上圖用鄰接矩陣表示如下:
1表示有邊,0表示沒有邊,也可以用布林變數true和false來表示。頂點與自身相連用 0 表示,所以這個矩陣從左上角到右上角的對角線全是 0 。
鄰接表:
鄰接表是一個連結串列陣列(或者是連結串列的連結串列),每個單獨的連結串列表示了有哪些頂點與當前頂點鄰接。
3、搜尋
在圖中實現最基本的操作之一就是搜尋從一個指定頂點可以到達哪些頂點,比如從武漢出發的高鐵可以到達哪些城市,一些城市可以直達,一些城市不能直達。現在有一份全國高鐵模擬圖,要從某個城市(頂點)開始,沿著鐵軌(邊)移動到其他城市(頂點),有兩種方法可以用來搜尋圖:深度優先搜尋(DFS)和廣度優先搜尋(BFS)。它們最終都會到達所有連通的頂點,深度優先搜尋通過棧來實現,而廣度優先搜尋通過佇列來實現,不同的實現機制導致不同的搜尋方式。
①、深度優先搜尋(DFS)
深度優先搜尋演算法有如下規則:
- 如果可能,訪問一個鄰接的未訪問頂點,標記它,並將它放入棧中。
- 當不能執行規則 1 時,如果棧不為空,就從棧中彈出一個頂點。
- 當不能執行規則 1 和規則 2 時,就完成了整個搜尋過程。
對於上圖,應用深度優先搜尋如下:假設選取 A 頂點為起始點,並且按照字母優先順序進行訪問,那麼應用規則 1 ,接下來訪問頂點 B,然後標記它,並將它放入棧中;再次應用規則 1,接下來訪問頂點 F,再次應用規則 1,訪問頂點 H。我們這時候發現,沒有 H 頂點的鄰接點了,這時候應用規則 2,從棧中彈出 H,這時候回到了頂點 F,但是我們發現 F 也除了 H 也沒有與之鄰接且未訪問的頂點了,那麼再彈出 F,這時候回到頂點 B,同理規則 1 應用不了,應用規則 2,彈出 B,這時候棧中只有頂點 A了,然後 A 還有未訪問的鄰接點,所有接下來訪問頂點 C,但是 C又是這條線的終點,所以從棧中彈出它,再次回到 A,接著訪問 D,G,I,最後也回到了 A,然後訪問 E,但是最後又回到了頂點 A,這時候我們發現 A沒有未訪問的鄰接點了,所以也把它彈出棧。現在棧中已無頂點,於是應用規則 3,完成了整個搜尋過程。
深度優先搜尋在於能夠找到與某一頂點鄰接且沒有訪問過的頂點。這裡以鄰接矩陣為例,找到頂點所在的行,從第一列開始向後尋找值為1的列;列號是鄰接頂點的號碼,檢查這個頂點是否未訪問過,如果是這樣,那麼這就是要訪問的下一個頂點,如果該行沒有頂點既等於1(鄰接)且又是未訪問的,那麼與指定點相鄰接的頂點就全部訪問過了(後面會用演算法實現)。
②、廣度優先搜尋(BFS)
深度優先搜尋要儘可能的遠離起始點,而廣度優先搜尋則要儘可能的靠近起始點,它首先訪問起始頂點的所有鄰接點,然後再訪問較遠的區域,這種搜尋不能用棧實現,而是用佇列實現。
- 訪問下一個未訪問的鄰接點(如果存在),這個頂點必須是當前頂點的鄰接點,標記它,並把它插入到佇列中。
- 如果已經沒有未訪問的鄰接點而不能執行規則 1 時,那麼從佇列列頭取出一個頂點(如果存在),並使其成為當前頂點。
- 如果因為佇列為空而不能執行規則 2,則搜尋結束。
對於上面的圖,應用廣度優先搜尋:以A為起始點,首先訪問所有與 A 相鄰的頂點,並在訪問的同時將其插入佇列中,現在已經訪問了 A,B,C,D和E。這時佇列(從頭到尾)包含 BCDE,已經沒有未訪問的且與頂點 A 鄰接的頂點了,所以從佇列中取出B,尋找與B鄰接的頂點,這時找到F,所以把F插入到佇列中。已經沒有未訪問且與B鄰接的頂點了,所以從佇列列頭取出C,它沒有未訪問的鄰接點。因此取出 D 並訪問 G,D也沒有未訪問的鄰接點了,所以取出E,現在佇列中有 FG,在取出 F,訪問 H,然後取出 G,訪問 I,現在佇列中有 HI,當取出他們時,發現沒有其它為訪問的頂點了,這時佇列為空,搜尋結束。
③、程式實現
package yrwan13;
import java.util.ArrayDeque;
import java.util.Deque;
public class Graph {
private Vertex[] vertexList;// 頂點陣列
private int[][] adjMat;// 鄰接矩陣
private int nVertex;// 當前數量
private Deque<Integer> stack;// 用棧實現深度優先搜尋
private Deque<Integer> queue;// 用佇列實現廣度優先搜尋
public Graph(int size) {
vertexList = new Vertex[size];
adjMat = new int[size][size];
// 初始化鄰接矩陣所有元素都為0,即所有頂點都沒有邊
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
adjMat[i][j] = 0;
}
}
nVertex = 0;// 初始化頂點個數為0
stack = new ArrayDeque<>();
queue = new ArrayDeque<>();
}
public void addVertex(char label) {
vertexList[nVertex++] = new Vertex(label);
}
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
adjMat[end][start] = 1;
}
public void displayAdjMat() {
for (int i = 0; i < nVertex; i++) {
for (int j = 0; j < nVertex; j++) {
System.out.print(adjMat[i][j] + " ");
}
System.out.println();
}
}
// 列印某個頂點表示的值
public void display(int v) {
System.out.print(vertexList[v].label);
}
// 找到與某一頂點鄰接且未被訪問的頂點
public int getAdjUnvisitedVertex(int v) {
for (int i = 0; i < nVertex; i++) {
if (adjMat[v][i] == 1 && vertexList[i].wasVisited == false) {
return i;
}
}
return -1;
}
/**
* 深度優先搜尋演算法:
* 1、用peek()方法檢查棧頂的頂點
* 2、用getAdjUnvisitedVertex()方法找到當前棧頂點鄰接且未被訪問的頂點
* 3、第二步方法返回值不等於-1則找到下一個未訪問的鄰接頂點,訪問這個頂點,併入棧;如果第二步方法返回值等於 -1,則沒有找到,出棧
*/
public void dfs() {
// 從第一個頂點開始訪問
vertexList[0].wasVisited = true;// 訪問之後標記為true
display(0);// 列印訪問的第一個頂點
stack.push(0);// 將第一個頂點放入棧中
while (!stack.isEmpty()) {
// 找到棧當前頂點鄰接且未被訪問的頂點
int v = getAdjUnvisitedVertex(stack.peek());
if (v == -1) {// 如果當前頂點值為-1,則表示沒有鄰接且未被訪問頂點,那麼出棧頂點
stack.pop();
} else {// 否則訪問下一個鄰接頂點
vertexList[v].wasVisited = true;
display(v);
stack.push(v);
}
}
// 搜尋完畢,重置所有標記位
for (int i = 0; i < nVertex; i++) {
vertexList[i].wasVisited = false;
}
}
/**
* 廣度優先搜尋演算法:
* 1、用remove()方法檢查棧頂的頂點
* 2、試圖找到這個頂點還未訪問的鄰節點
* 3、 如果沒有找到,該頂點出列
* 4、 如果找到這樣的頂點,訪問這個頂點,並把它放入佇列中
*/
public void bfs() {
vertexList[0].wasVisited = true;
display(0);
queue.offer(0);
while (!queue.isEmpty()) {
int temp = queue.poll();
int v;
while ((v = getAdjUnvisitedVertex(temp)) != -1) {
vertexList[v].wasVisited = true;
display(v);
queue.offer(v);
}
}
// 搜尋完畢,重置所有標記位
for (int i = 0; i < nVertex; i++) {
vertexList[i].wasVisited = false;
}
}
}