資料結構與演算法(十八):圖
一、什麼是圖
1.概述
首先,我們已經在之前學習過了樹這種資料結構,樹能反映一對多的關係,但是卻無法反映多對多的關係,因此我們引入了圖這種資料結構。
對於圖,其節點也可以叫做頂點,每個節點具有零或者多個相連節點,每個節點之間的連線稱為邊,從一個節點到達另一個節點路線都稱為路徑。
以上圖為例,其中:
- 無向圖:頂點之間連線沒有方向。比如從A到C,可是A -> B -> C,也可以是A -> D -> B -> C。
- 有向圖:頂點之間連線有方向。如果A到B,必須是A -> B,不能是B -> A
- 帶權圖:邊帶有權值。
2.樹與圖的關係
實際上,對於有向圖還分為兩種情況,即圖中含環或者圖中不含環的單向圖,其中含環的圖可以從某個頂點出發最終返回原點。
結合對圖的定義,我們不難發現,樹也可以理解為不含有環的單向圖,是圖的子集。
兩者的區別在於:
- 圖中每個節點可以有任意數量的邊,而樹兩個節點間僅僅只有一條邊
- 圖沒有根節點,而樹有
- 圖中可以存著環,而樹不行
- 如果有n個節點,圖最多有n*(n-1)條邊,而樹最多有n-1條邊
二、圖的表示與構建
圖的表示就是邊與邊關係的表示,有二維陣列(鄰接矩陣)和連結串列(鄰接表)兩種表示方法。
1.鄰接矩陣
我們建立一個二維陣列(矩陣),第一維表示頂點,而第二維表示與該頂點相連線的點。
比如說0號點與1,2,3,4相連,與0(自己)和5不相連,表示為[0][011110]
,其中,二維陣列中的1表示與0號點相連,0表示與0號點不相連
2.鄰接表
鄰接表相比鄰接矩陣,只表示關聯的邊而不表示不關聯的表,相對鄰接矩陣而言更簡潔也更節省空間
3.程式碼實現
我們使用鄰接矩陣的方式來示範如何使用程式碼構建一個圖。
為了方便理解,我們使用兩個陣列來表示節點與節點之間的對應關係:
如上圖,上圖的節點之間的對應關係通過兩個陣列來表示就是{0,0,0,0,1} -> {1,2,3,4,2}
,即 0->1,0->2,,0->3,,0->4,,1->2
,可見要建立的圖有5個節點。
對應實現程式碼如下:
/** * @Author:CreateSequence * @Date:2020-08-04 16:50 * @Description:圖 */ public class Graph { //節點與節點間的相連關係 private int[] node1; private int[] node2; //有幾個節點 private int num; //邊的數量 private int sideNum; private int[][] graph; public Graph(int[] node1, int[] node2, int num) { this.node1 = node1; this.node2 = node2; this.num = num; this.sideNum = 0; //建立圖 CreateGraph(); } /** * 建立圖 */ private void CreateGraph(){ //獲取二維陣列,一維表示節點,二維表示節點的相鄰節點 graph = new int[num][num]; //初始化陣列 for (int i = 0; i < num; i++) { graph[i] = Arrays.copyOf(graph[i], num); } //新增節點 for (int i = 0; i < node1.length; i++) { //統計邊數 if (graph[node1[i]][node2[i]] == 0) { sideNum++; } graph[node1[i]][node2[i]] = 1; graph[node2[i]][node1[i]] = 1; } } /** * 展示圖 */ public void show() { for (int[] n1 : graph) { for (int n2 : n1) { System.out.print(n2 + " "); } System.out.println(); } System.out.println("有" + num + "個節點," + sideNum + "條邊"); } } //輸出 0 1 1 1 1 1 0 1 0 0 1 1 0 0 0 1 0 0 0 0 1 0 0 0 0 有5個節點,5條邊
三、圖的深度優先搜尋
圖的遍歷有兩種策略:深度優先搜尋(DFS)和廣度優先搜尋(BFS)。
以下的演示我們仍基於第二部分建立的圖為示例:
1.思路分析
dfs的搜尋大體思路是這樣的:
首先訪問第一個鄰接結點,然後再以這個被訪問的鄰接結點作為初始結點,訪問它的第一個鄰接結點,然後重複以上步驟直到完成遍歷。
這個思路如果學過樹的遍歷會感覺非常熟悉。由前面知道,樹就是一種特殊的圖,所以樹的前、中、後序遍歷其實就是樹的dfs。
2.程式碼實現
將思路轉換為程式碼實現的步驟:
- 訪問第一個節點v,並且將其標記為已訪問
- 查詢第一個節點的鄰接節點w:
- 如果w節點不存在,則繼續查詢v的下一個鄰接節點
- 如果w存在,並且未訪問,則將w當成下一個v,進行遞迴
第一步,我們需要在Graph類
中新增isVisted
公共變數用於標記節點是否被訪問:
//記錄節點是否被訪問
private boolean[] isVisted;
第二步,我們需要查詢節點是否存在相連節點方法
/**
* 查詢鄰接節點
* @param index
* @return
*/
private int getNeighbor(int index) {
for (int i = 0; i < graph.length; i++) {
//如果當前節點存在鄰接節點就返回下標
if (graph[index][i] > 0) {
return i;
}
}
return -1;
}
/**
* 查詢下一個鄰接節點的下標
* @param index1
* @param index2
* @return
*/
private int getNextNeighbor(int index1, int index2) {
for (int i = index2 + 1; i < graph.length; i++) {
//如果當前節點存在鄰接節點就返回下標
if (graph[index1][index2] > 0) {
return i;
}
}
return -1;
}
第三步,藉助訪問標記和查詢鄰接節點方法實現dfs
/**
* 深度優先搜尋
* @param index
*/
private void dsf(int index) {
//訪問節點
System.out.print(index + "->");
//標記已訪問節點
isVisted[index] = true;
//獲取第一個鄰接節點
int w = getNeighbor(index);
//如果鄰接節點存在
while (w != -1){
//並且該鄰接節點未訪問
if (!isVisted[w]) {
dsf(w);
}
//如果該節點已被訪問,就訪問當前節點的鄰接節點的下一個鄰接節點
w = getNextNeighbor(index, w);
}
}
public void dfs() {
//對所有節點進行dfs
for (int i = 0; i < num; i++) {
//如果該節點仍未被訪問才進行dfs
if (!isVisted[i]) {
dsf(i);
}
}
}
//執行結果
0->1->2->3->4->
四、圖的廣度優先搜尋
1.思路分析
bfs的大題思路是這樣的:
首先建立一個佇列,把第一個鄰接節點入隊,然後佇列元素出隊,把該元素的鄰接節點入隊,然後出隊.....重複該步驟,一層一層的遍歷同級節點
如果我們按這個思路,將4作為起始節點,那麼第一個4入隊,然後4出隊,把4的鄰接節點0入隊,接著0出隊,把0的鄰接節點1,2,3,入隊;同理如果將0作為起始節點,那麼第一次0入隊,然後0出隊,把0的鄰接節點1,2,3入隊......
2.程式碼實現
將思路轉換為程式碼實現的步驟:
- 訪問初始節點v,標記併入隊
- 當佇列不為空時,將隊頭節點u出隊,否則跳過本次迴圈
- 查詢u的第一個鄰接節點w,如果不存在就重複步驟2,否則:
- 若w未被訪問,則標記併入隊
- 查詢u繼w後的下一個鄰接節點,重複步驟3
這裡繼續複用上文dfs中使用的 getNeighbor()
、getNextNeighbor()
和 isVisted[]
/**
* 廣度優先遍歷
* @param index
*/
private void bfs(int index){
//建立佇列
LinkedList queue = new LinkedList<>();
//訪問節點
System.out.print(index + "->");
//標記已訪問節點
isVisted[index] = true;
//節點入隊
queue.addLast(index);
//迴圈直到遍歷完所有佇列中的節點
int u, w = -1;
while (queue.isEmpty()) {
//取出佇列頭結點下標
u = (int) queue.removeFirst();
//獲取出隊節點的鄰接節點
w = getNeighbor(u);
while (w != -1) {
//如果為被訪問過
if (!isVisted[w]) {
//訪問節點並標記
System.out.print(u + "->");
isVisted[w] = true;
//將節點入隊
queue.addLast(w);
}
//接著查詢下一個鄰接節點
w = getNextNeighbor(u, w);
}
}
}
public void bfs() {
this.isVisted = new boolean[num];
//對所有節點進行bfs
for (int i = 0; i < num; i++) {
//如果該節點仍未被訪問才驚喜dfs
if (!isVisted[i]) {
bfs(i);
}
}
}
//執行結果
0->1->2->3->4->
值得一提是,雖然上文的例子不太直觀,但是bfs也常常用於樹的層次遍歷,比如
//測試資料
int num = 9;
int[] u = {0, 0, 1, 2, 3, 3, 4, 4};
int[] v = {1, 2, 3, 4, 5, 6, 7, 8};
//輸出結果
0->1->2->3->4->5->6->7->8->
可以很明顯的看出,是一層一層遍歷的,這也很直觀的反應了bfs的執行邏輯。