不要你覺得,我要我覺得,我說圖它不是個東西。——明人明言。
為什么有圖
用來表示多對多的關系。
線性表局限於一個直接前驅和一個直接后繼的關系
樹也只能有一個直接前驅也就是父節點
基本概念
邊:兩結點的連線
頂點(vertex):數據元素,一個頂點可以具有零個或多個相鄰元素。
路徑: 比如從 D-> C 的路徑有
1)D->B->C
2)D->A->B->C
分類
無向圖:如上圖,頂點間連線無方向。比如A-B,即可以是 A-> B 也可以 B->A .
有向圖:頂點之間的連接有方向,比如A-B,
只能是 A-> B 不能是 B->A
帶權圖:這種邊帶權值的圖也叫網
表示方式
或者也就存儲結構
圖的表示方式有兩種:二維數組表示(鄰接矩陣);鏈表表示(鄰接表)。
鄰接矩陣
我們用兩個數組來表示圖:
一維數組用來存儲圖中頂點信息
二維數組存放存放圖中邊信息
求頂點vi的所有鄰接點就是將矩陣中第i行元素掃描一遍,arr[i][j]
為1的就是鄰接點。
鄰接矩陣是表示圖形中頂點之間相鄰關系的矩陣,對於n個頂點的圖而言,矩陣是row和col表示的是1....n個點。
下面是無向圖的一個例子,觀察:
鄰接矩陣是對稱矩陣
主對角線為0,不存在頂點到自身的邊;
鄰接表
只關心存在的邊。
因為鄰接矩陣需要為每個頂點都分配n個邊的空間,其實有很多邊都是不存在,會造成空間的一定損失.
鄰接表的實現只關心存在的邊,不關心不存在的邊。因此沒有空間浪費,鄰接表由數組和鏈表組成。
創建一個圖
代碼實現如下圖結構
A B C D E
A 0 1 1 0 0
B 1 0 1 1 1
C 1 1 0 0 0
D 0 1 0 0 0
E 0 1 0 0 0
//說明
//(1) 1 表示能夠直接連接
//(2) 0 表示不能直接連接
思路分析:
需要兩個數組
String類型的ArrayList用來存儲頂點
二維數組來保存邊信息
常用方法:
- 插入頂點
- 插入邊
- 返回結點的個數
- 得到邊的數目,每插入邊就累加一次
- 顯示圖對應的矩陣
- 返回結點i(下標)對應的數據 0->"A" 1->"B" 2->"C"
- 返回v1和v2的權值,該權值存在數組里。
代碼實現
import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; /** * @ClassName: Demo20_Graph * @author: benjamin * @version: 1.0 * @description: TODO * @createTime: 2019/08/26/11:20 */ public class Demo20_Graph { // 用於存儲頂點的集合 private ArrayList<String> vertexList; // 存儲邊的信息的二維數組 private int[][] edges; // 記錄邊的數目 private int numOfEdges; // 定義數組boolean[],記錄某個結點是否被訪問 public static void main(String[] args) { Demo20_Graph graph = new Demo20_Graph(5); // 插入頂點 String vertexs[] = {"A","B","C","D","E","F"}; for(String vertex:vertexs){ graph.insertVertex(vertex); } // 添加邊 // A-B A-C B-C B-D B-E graph.insertEdge(0,1,1); graph.insertEdge(0,2,1); graph.insertEdge(1,2,1); graph.insertEdge(1,3,1); graph.insertEdge(1,4,1); // 顯示一把鄰結矩陣 graph.showGraph(); } // 構造器,初始化矩陣和頂點集合 Demo20_Graph(int n) { // 集合長度為n vertexList = new ArrayList<String>(n); // 鄰接矩陣為n*n edges = new int[n][n]; numOfEdges = 0; } //常用的方法 //返回結點的個數 public int getNumOfVertex() { return vertexList.size(); } //顯示圖對應的矩陣 public void showGraph() { for(int[] vertex:edges){ System.out.println(Arrays.toString(vertex)); } } //得到邊的數目 public int getNumOfEdges() { return numOfEdges; } //返回結點i(下標)對應的數據 0->"A" 1->"B" 2->"C" public String getValueByIndex(int i) { return vertexList.get(i); } //返回v1和v2的權值 public int getWeight(int v1, int v2) { return edges[v1][v2]; } //插入結點 public void insertVertex(String vertex) { vertexList.add(vertex); } /** * 添加邊 * * @param v1 表示點的下標即使第幾個頂點 "A"-"B" "A"->0 "B"->1 * @param v2 第二個頂點對應的下標 * @param weight 表示權,1或者0 */ public void insertEdge(int v1, int v2, int weight) { edges[v1][v2] = weight; edges[v2][v1] = weight; numOfEdges++; } }
輸出:
[0, 1, 1, 0, 0]
[1, 0, 1, 1, 1]
[1, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 1, 0, 0, 0]
圖的遍歷
即結點的訪問。一個圖有那么多個結點,如何遍歷這些結點,需要特定策略,一般有兩種訪問策略:
- 深度優先遍歷
- 廣度優先遍歷
前者選擇一個領域精通后,再回來進行研究下一個領域;
后者像創業,從已知出發, 從已經知道的東西逐漸再挖掘感興趣的部分;
深度優先遍歷(DFS)
基本思想
圖的深度優先搜索(Depth First Search)
從初始訪問結點出發,初始訪問結點可能有多個鄰接結點,深度優先遍歷的策略就是首先訪問第一個鄰接結點,然后再以這個被訪問的鄰接結點作為初始結點,訪問它的第一個鄰接結點,可以這樣理解:每次都在訪問完當前結點后首先訪問當前結點的第一個鄰接結點。
我們可以看到,這樣的訪問策略是優先往縱向挖掘深入,深度優先搜索是一個遞歸的過程
如何實現上圖中的DFS步驟呢?
初始結點為A,從A出發,先標記A已訪問,
A的第一個鄰接結點為B,B存在而且未被訪問,現在把B當做初始結點
從B出發,先標記B已訪問,B的第一個鄰接結點為C,C存在而且未被訪問,現在把C當做初始結點
C 結點的下一個結點D不存在,此時回到B,B的下一個鄰接結點尾E
如何實現上圖中的DFS步驟呢?
初始結點為A,從A出發,先標記A已訪問,
A的第一個鄰接結點為B,B存在而且未被訪問,現在把B當做初始結點
從B出發,先標記B已訪問,B的第一個鄰接結點為C,C存在而且未被訪問,現在把C當做初始結點
C 結點的下一個結點D不存在,此時回到B,B的下一個鄰接結點尾E
思路分析:詳解A和B的恩怨糾纏。
以A為初識結點,A出發,標記A已經訪問,如何標記它被訪問過呢?即定義一個boolean的數組,把A的下標的元素置為true;
找到A的下一個鄰接結點B,如何找呢?首先我們需要有A的下標,然后找到與A相連的邊的長度大於0的結點,也就是說,需要首先遍歷A這一行的數組(因為該矩陣中存放的是邊的信息),遍歷的長度是多少呢?明顯是頂點集合的長度,只要找到數組中元素大於1的位置,直接返回其坐標。否則就是沒有與A相連的頂點,就返回-1;
找到的鄰接結點B的下標,只要它不為-1,即就是A有相連的鄰接結點,我們又不能保證它是不是被訪問過,所以首先需要判斷B是否被訪問過,拿到B的下標去boolean數組中判斷,為false,我們就讓以B為初識結點進行dfs;如果B被訪問過,那好辦,我們找到A的鄰接結點B的下一個鄰接結點C。
這里如何找到A的鄰接結點B的下一個鄰接結點C?首先我們把A的位置,B的位置傳進去,A用來控制找的是A的鄰接結點,B用來控制找到的是B的下一個節點,也就是從B的位置+1處開始進行遍歷;
算法步驟
- 訪問初始結點v,並標記結點v為已訪問。
- 查找結點v的第一個鄰接結點w。
- 若w存在,則繼續執行4,如果w不存在,則回到第1步,將從v的下一個結點繼續。
- 若w未被訪問,對w進行深度優先遍歷遞歸(即把w當做另一個v,然后進行步驟123)。
- 查找結點v的w鄰接結點的下一個鄰接結點,轉到步驟3。
步驟詳解:
- 初識A的位置為0,標記A,輸出0對應的頂點集合中的A,將A的位置0加到隊列中;
- 只要隊列不為空,就取出隊列的頭部,即取出0,找到0對應的下一個結點位置,即就是B的位置為1,
- 這里需要判斷B的位置是否存在,如果不為-1就存在,輸出1對應的B元素,標記B,將B加入隊尾;
- 如果B已經被訪問了,我們就需要去A的下一個結點B的下一個結點C處去找鄰結點。需要以A的位置作為行號,B的位置+1作為遍歷起始位置,遍歷的長度需要小於頂點集合的長度。
代碼實現
/**
* 得到第一個鄰接結點的下標 w
*
* @return 如果存在就返回對應的下標,否則返回-1
*/
public int getFirstNeighbor(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (edges[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* @Description: 根據前一個鄰接結點的下標來獲取下一個鄰接結點
* @Param: [v1, v2]
* @return: int
* @Author: benjamin
* @Date: 2019/8/26
*/
public int getNextNeighbor(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (edges[v1][j] > 0) {
return j;
}
}
return -1;
}
//深度優先遍歷算法
//i 第一次就是 0
private void dfs(boolean[] isVisited, int i) {
// 首先輸出訪問的結點
System.out.print(getValueByIndex(i) + "->");
// 將結點設置為已經訪問
isVisited[i] = true;
// 查找結點i的第一個鄰接結點w
int w = getFirstNeighbor(i);
// 只要w不為-1,說明有
while (w != -1){
if(!isVisited[w]){
dfs(isVisited,w);
}
// 如果該結點已經被訪問過,則訪問i的下下一個鄰接結點
w = getNextNeighbor(i,w);
}
}
//對dfs 進行一個重載, 遍歷我們所有的結點,並進行 dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍歷所有的結點,長度為集合的長度,進行dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
dfs(isVisited, i);
}
}
}
#### 廣度優先遍歷(BFS)
基本思想
圖的廣度優先搜索(Broad First Search) 。
類似於一個分層搜索的過程,廣度優先遍歷需要使用一個隊列以保持訪問過的結點的順序,以便按這個順序來訪問這些結點的鄰接結點
算法步驟
1. 訪問初始結點v並標記結點v為已訪問。
2. 結點v入隊列
3. 當隊列非空時,繼續執行,否則算法結束。
4. 出隊列,取得隊頭結點u。
5. 查找結點u的第一個鄰接結點w。
6. 若結點u的鄰接結點w不存在,則轉到步驟3;否則循環執行以下三個步驟:
1. 若結點w尚未被訪問,則訪問結點w並標記為已訪問。
2. 結點w入隊列
3. 查找結點u的繼w鄰接結點后的下一個鄰接結點w,轉到步驟6。
代碼實現
```java
/**
* 得到第一個鄰接結點的下標 w
*
* @return 如果存在就返回對應的下標,否則返回-1
*/
public int getFirstNeighbor(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (edges[index][j] > 0) {
return j;
}
}
return -1;
}
/**
* @Description: 根據前一個鄰接結點的下標來獲取下一個鄰接結點
* @Param: [v1, v2]
* @return: int
* @Author: benjamin
* @Date: 2019/8/26
*/
public int getNextNeighbor(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (edges[v1][j] > 0) {
return j;
}
}
return -1;
}
//對一個結點進行廣度優先遍歷的方法
private void bfs(boolean[] isVisited, int i) {
int u ; // 表示隊列的頭結點對應下標
int w ; // 鄰接結點w
//隊列,記錄結點訪問的順序
LinkedList queue = new LinkedList();
//訪問結點,輸出結點信息
System.out.print(getValueByIndex(i) + "=>");
//標記為已訪問
isVisited[i] = true;
//將結點加入隊列
queue.addLast(i);
while( !queue.isEmpty()) {
//取出隊列的頭結點下標
u = (Integer)queue.removeFirst();
//得到第一個鄰接結點的下標 w
w = getFirstNeighbor(u);
while(w != -1) {//找到
//是否訪問過
if(!isVisited[w]) {
System.out.print(getValueByIndex(w) + "=>");
//標記已經訪問
isVisited[w] = true;
//入隊
queue.addLast(w);
}
//以u為前驅點,找w后面的下一個鄰結點
w = getNextNeighbor(u, w); //體現出我們的廣度優先
}
}
}
//遍歷所有的結點,都進行廣度優先搜索
public void bfs() {
isVisited = new boolean[vertexList.size()];// 清空標志位
for (int j = 0; j < vertexList.size(); j++) {
if (!isVisited[j]) {
bfs(isVisited, j);
}
}
}