第12章 圖
本章源碼:https://github.com/name365/Java-Data-structure
圖的基本介紹和存儲形式
圖基本介紹
爲什麼要有圖??? 前面我們學了線性表和樹,線性表侷限於一個直接前驅和一個直接後繼的關係,樹也只能有一個直接前驅也就是父節點。當我們需要表示多對多的關係時,這裏我們就用到了圖。
圖的舉例說明
- 圖是一種數據結構,其中結點可以具有零個或多個相鄰元素。兩個結點之間的連接稱爲邊。 結點也可以稱爲頂點。如圖:
l圖的常用概念:
- 頂點(vertex)
- 邊(edge)
- 路徑:比如從 D -> C 的路徑有
- D->B->C
- D->A->B->C
- 無向圖(上圖):頂點之間的連接沒有方向,比如A-B,即可以是 A-> B 也可以 B->A .
- 有向圖:頂點之間的連接有方向,比如A-B,只能是 A-> B 不能是 B->A .
- 帶權圖:這種邊帶權值的圖也叫網.
圖的表示方式
- 圖的表示方式有兩種:二維數組表示(鄰接矩陣);鏈表表示(鄰接表)。
鄰接矩陣
鄰接矩陣是表示圖形中頂點之間相鄰關係的矩陣,對於n個頂點的圖而言,矩陣是的row和col表示的是1....n個點。
鄰接表:
1.鄰接矩陣需要爲每個頂點都分配n個邊的空間,其實有很多邊都是不存在,會造成空間的一定損失. 2.鄰接表的實現只關心存在的邊,不關心不存在的邊。因此沒有空間浪費,鄰接表由數組+鏈表組成
說明:
1.標號爲0的結點的相關聯的結點爲 1 2 3 4
2.標號爲1的結點的相關聯結點爲0 4,
3.標號爲2的結點相關聯的結點爲 0 4 5
4.....
圖的創建圖解和代碼實現
要求: 代碼實現如下圖結構.
思路分析
(1) 存儲頂點String 使用 ArrayList
(2) 保存矩陣 int[][] edges
代碼實現如下:
import java.util.ArrayList;
import java.util.Arrays;
public class Graph {
private ArrayList<String> vertexList; // 存儲頂點集合
private int[][] str; // 存儲圖對應的鄰接矩陣
private int numofEdges; // 表示邊的數目
public static void main(String[] args) {
// 測試圖的創建
int n = 5; // 結點的個數
String Ver[] = { "A", "B", "C", "D", "E" };
// 創建圖對象
Graph graph = new Graph(n);
// 循環的添加頂點
for (String vertex : Ver) {
graph.insertVertex(vertex);
}
//添加邊
//A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1); // A-B
graph.insertEdge(0, 2, 1); // A-C
graph.insertEdge(1, 2, 1); // B-C
graph.insertEdge(1, 3, 1); // B-D
graph.insertEdge(1, 4, 1); // B-E
//顯示鄰接矩陣
graph.showGraph();
}
// 構造器
public Graph(int n) {
// 初始化矩陣和vertexList
str = new int[n][n];
vertexList = new ArrayList<String>(n);
numofEdges = 0;
}
// 插入結點
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
// 添加邊
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午6:37:03
* @param v1
* 表示點的下標即第幾個頂點 "A"-"B" "A"->0 "B"->1
* @param v2
* 第二個頂點對應的下標
* @param weight
* 表示
*/
public void insertEdge(int v1, int v2, int weight) {
str[v1][v2] = weight;
str[v2][v1] = weight;
numofEdges++;
}
// 圖中常用的方法
// 返回結點的個數
public int getNumber() {
return vertexList.size();
}
// 得到邊的個數
public int getNumEdg() {
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 str[v1][v2];
}
// 返回結點的個數
public int getNumOfVertex() {
return vertexList.size();
}
// 顯示圖對應的矩陣
public void showGraph() {
for (int[] link : str) {
System.err.println(Arrays.toString(link));
}
}
}
圖的深度優先(DFS)算法圖解與實現
圖遍歷介紹
所謂圖的遍歷,即是對結點的訪問。一個圖有那麼多個結點,如何遍歷這些結點,需要特定策略,一般有兩種訪問策略: (1) (2)
-
深度優先遍歷基本思想
圖的深度優先搜索(Depth First Search) 。 1.深度優先遍歷,從初始訪問結點出發,初始訪問結點可能有多個鄰接結點,深度優先遍歷的策略就是首先訪問第一個鄰接結點,然後再以這個被訪問的鄰接結點作爲初始結點,訪問它的第一個鄰接結點, 可以這樣理解:每次都在訪問完當前結點後首先訪問當前結點的第一個鄰接結點。 2.我們可以看到,這樣的訪問策略是優先往縱向挖掘深入,而不是對一個結點的所有鄰接結點進行橫向訪問。 3.顯然,深度優先搜索是一個遞歸的過程。
深度優先遍歷算法步驟: 1.訪問初始結點v,並標記結點v爲已訪問。 2.查找結點v的第一個鄰接結點w。 3.若w存在,則繼續執行4,如果w不存在,則回到第1步,將從v的下一個結點繼續。 4.若w未被訪問,對w進行深度優先遍歷遞歸(即把w當做另一個v,然後進行步驟123)。 5.查找結點v的w鄰接結點的下一個鄰接結點,轉到步驟3。
看一個具體案例分析:
要求:對下圖進行深度優先搜索, 從A 開始遍歷。
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 表示不能直接連接
代碼實現如下:
import java.util.ArrayList;
import java.util.Arrays;
public class Graph {
private ArrayList<String> vertexList; // 存儲頂點集合
private int[][] str; // 存儲圖對應的鄰接矩陣
private int numofEdges; // 表示邊的數目
// 定義一個數組boolean[], 記錄某個結點是否被訪問
private boolean[] isNut;
public static void main(String[] args) {
// 測試圖的創建
int n = 5; // 結點的個數
String Ver[] = { "A", "B", "C", "D", "E" };
// 創建圖對象
Graph graph = new Graph(n);
// 循環的添加頂點
for (String vertex : Ver) {
graph.insertVertex(vertex);
}
// 添加邊
// A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1); // A-B
graph.insertEdge(0, 2, 1); // A-C
graph.insertEdge(1, 2, 1); // B-C
graph.insertEdge(1, 3, 1); // B-D
graph.insertEdge(1, 4, 1); // B-E
// 顯示鄰接矩陣
graph.showGraph();
//測試dfs
System.out.println("深度遍歷:");
graph.dfs(); //A->B->C->D->E->
}
// 構造器
public Graph(int n) {
// 初始化矩陣和vertexList
str = new int[n][n];
vertexList = new ArrayList<String>(n);
numofEdges = 0;
isNut = new boolean[5];
}
// 插入結點
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
// 添加邊
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午6:37:03
* @param v1
* 表示點的下標即第幾個頂點 "A"-"B" "A"->0 "B"->1
* @param v2
* 第二個頂點對應的下標
* @param weight
* 表示
*/
public void insertEdge(int v1, int v2, int weight) {
str[v1][v2] = weight;
str[v2][v1] = weight;
numofEdges++;
}
// 圖中常用的方法
// 返回結點的個數
public int getNumber() {
return vertexList.size();
}
// 得到邊的個數
public int getNumEdg() {
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 str[v1][v2];
}
// 返回結點的個數
public int getNumOfVertex() {
return vertexList.size();
}
// 顯示圖對應的矩陣
public void showGraph() {
for (int[] link : str) {
System.err.println(Arrays.toString(link));
}
}
// 得到第一個鄰接結點的下標 w
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午8:03:22
* @param index
* @return 如果存在就返回對應的下標,否則返回-1
*/
public int getFirstNeigh(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (str[index][j] > 0) {
return j;
}
}
return -1;
}
// 根據前一個鄰接結點的下標來獲取下一個鄰接結點
public int getNextNeigh(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (str[v1][j] > 0) {
return j;
}
}
return -1;
}
// 深度優先算法
public void dfs(boolean[] isNut, int i) {
// 首先我們訪問該結點,輸出
System.out.print(getValueByIndex(i) + "->");
// 將結點設置爲已經訪問
isNut[i] = true;
// 查找結點i的第一個鄰接結點w
int w = getFirstNeigh(i);
while (w != -1) {// 說明找到了
if (!isNut[w]) {
dfs(isNut, w);
}
// 如果w結點已經被訪問過
w = getNextNeigh(i, w);
}
}
//對DFS進行重載,遍歷所有的結點,並進行DFS
public void dfs() {
isNut = new boolean[vertexList.size()];
//遍歷所有的結點,進行dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isNut[i]) {
dfs(isNut, i);
}
}
}
}
圖的廣度優先(BFS)算法圖解
廣度優先遍歷基本思想
圖的廣度優先搜索(Broad First Search) 。
類似於一個分層搜索的過程,廣度優先遍歷需要使用一個隊列以保持訪問過的結點的順序,以便按這個順序來訪問這些結點的鄰接結點
廣度優先遍歷算法步驟:
1.訪問初始結點v並標記結點v爲已訪問。
2.結點v入隊列
3.當隊列非空時,繼續執行,否則算法結束。
4.出隊列,取得隊頭結點u。
5.查找結點u的第一個鄰接結點w。
6.若結點u的鄰接結點w不存在,則轉到步驟3;否則循環執行以下三個步驟:
6.1 若結點w尚未被訪問,則訪問結點w並標記爲已訪問。
6.2 結點w入隊列
6.3 查找結點u的繼w鄰接結點後的下一個鄰接結點w,轉到步驟6。
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 表示不能直接連接
代碼實現如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
public class Graph {
private ArrayList<String> vertexList; // 存儲頂點集合
private int[][] str; // 存儲圖對應的鄰接矩陣
private int numofEdges; // 表示邊的數目
// 定義一個數組boolean[], 記錄某個結點是否被訪問
private boolean[] isNut;
public static void main(String[] args) {
// 測試圖的創建
int n = 5; // 結點的個數
String Ver[] = { "A", "B", "C", "D", "E" };
// 創建圖對象
Graph graph = new Graph(n);
// 循環的添加頂點
for (String vertex : Ver) {
graph.insertVertex(vertex);
}
// 添加邊
// A-B A-C B-C B-D B-E
graph.insertEdge(0, 1, 1); // A-B
graph.insertEdge(0, 2, 1); // A-C
graph.insertEdge(1, 2, 1); // B-C
graph.insertEdge(1, 3, 1); // B-D
graph.insertEdge(1, 4, 1); // B-E
// 顯示鄰接矩陣
graph.showGraph();
//測試bfs
System.out.println("廣度遍歷:");
graph.bfs(); //A=>B=>C=>D=>E=>
}
// 構造器
public Graph(int n) {
// 初始化矩陣和vertexList
str = new int[n][n];
vertexList = new ArrayList<String>(n);
numofEdges = 0;
isNut = new boolean[5];
}
// 插入結點
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
// 添加邊
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午6:37:03
* @param v1
* 表示點的下標即第幾個頂點 "A"-"B" "A"->0 "B"->1
* @param v2
* 第二個頂點對應的下標
* @param weight
* 表示
*/
public void insertEdge(int v1, int v2, int weight) {
str[v1][v2] = weight;
str[v2][v1] = weight;
numofEdges++;
}
// 圖中常用的方法
// 返回結點的個數
public int getNumber() {
return vertexList.size();
}
// 得到邊的個數
public int getNumEdg() {
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 str[v1][v2];
}
// 返回結點的個數
public int getNumOfVertex() {
return vertexList.size();
}
// 顯示圖對應的矩陣
public void showGraph() {
for (int[] link : str) {
System.err.println(Arrays.toString(link));
}
}
// 得到第一個鄰接結點的下標 w
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午8:03:22
* @param index
* @return 如果存在就返回對應的下標,否則返回-1
*/
public int getFirstNeigh(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (str[index][j] > 0) {
return j;
}
}
return -1;
}
// 根據前一個鄰接結點的下標來獲取下一個鄰接結點
public int getNextNeigh(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (str[v1][j] > 0) {
return j;
}
}
return -1;
}
// 廣度優先算法
public void bfs(boolean[] isNut, int i) {
int u ; // 表示隊列的頭結點對應下標
int w ; // 鄰接結點w
//隊列,記錄結點訪問的順序
LinkedList<Integer> queue = new LinkedList<Integer>();
// 訪問結點,輸出結點信息
System.out.print(getValueByIndex(i) + "=>");
// 標記爲已訪問
isNut[i] = true;
// 將結點加入隊列
queue.addLast(i);
while( !queue.isEmpty()) {
// 取出隊列的頭結點下標
u = (Integer)queue.removeFirst();
// 得到第一個鄰接結點的下標 w
w = getFirstNeigh(u);
while(w != -1) {//找到了
// 是否訪問過
if(!isNut[w]) {
System.out.print(getValueByIndex(w) + "=>");
// 標記已經訪問
isNut[w] = true;
// 入隊
queue.addLast(w);
}
// 以u爲前驅點,找w後面的下一個鄰結點
w = getNextNeigh(u, w); // 體現廣度優先
}
}
}
//對BFS進行重載,遍歷所有的結點,並進行BFS
public void bfs() {
isNut = new boolean[vertexList.size()];
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isNut[i]) {
bfs(isNut, i);
}
}
}
}
圖的DFS和BFS比較
graph.insertEdge(0, 1, 1); graph.insertEdge(0, 2, 1); graph.insertEdge(1, 3, 1); graph.insertEdge(1, 4, 1); graph.insertEdge(3, 7, 1); graph.insertEdge(4, 7, 1); graph.insertEdge(2, 5, 1); graph.insertEdge(2, 6, 1); graph.insertEdge(5, 6, 1); 深度優先遍歷順序爲 1->2->4->8->5->3->6->7 廣度優先算法的遍歷順序爲:1->2->3->4->5->6->7->8
代碼實現如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
public class Graph {
private ArrayList<String> vertexList; // 存儲頂點集合
private int[][] str; // 存儲圖對應的鄰接矩陣
private int numofEdges; // 表示邊的數目
// 定義一個數組boolean[], 記錄某個結點是否被訪問
private boolean[] isNut;
public static void main(String[] args) {
// 測試圖的創建
int n = 8; // 結點的個數
String Ver[] = {"1", "2", "3", "4", "5", "6", "7", "8"};
// 創建圖對象
Graph graph = new Graph(n);
// 循環的添加頂點
for (String vertex : Ver) {
graph.insertVertex(vertex);
}
// 添加邊
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
// 顯示鄰接矩陣
graph.showGraph();
// 測試dfs
System.out.println("深度遍歷:");
graph.dfs(); // 1->2->4->8->5->3->6->7->
// 測試bfs
System.out.println("廣度遍歷:");
graph.bfs(); // 1=>2=>3=>4=>5=>6=>7=>8=>
}
// 構造器
public Graph(int n) {
// 初始化矩陣和vertexList
str = new int[n][n];
vertexList = new ArrayList<String>(n);
numofEdges = 0;
isNut = new boolean[5];
}
// 插入結點
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
// 添加邊
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午6:37:03
* @param v1
* 表示點的下標即第幾個頂點 "A"-"B" "A"->0 "B"->1
* @param v2
* 第二個頂點對應的下標
* @param weight
* 表示
*/
public void insertEdge(int v1, int v2, int weight) {
str[v1][v2] = weight;
str[v2][v1] = weight;
numofEdges++;
}
// 圖中常用的方法
// 返回結點的個數
public int getNumber() {
return vertexList.size();
}
// 得到邊的個數
public int getNumEdg() {
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 str[v1][v2];
}
// 返回結點的個數
public int getNumOfVertex() {
return vertexList.size();
}
// 顯示圖對應的矩陣
public void showGraph() {
for (int[] link : str) {
System.err.println(Arrays.toString(link));
}
}
// 得到第一個鄰接結點的下標 w
/**
*
* @Description
* @author subei
* @date 2020年6月14日下午8:03:22
* @param index
* @return 如果存在就返回對應的下標,否則返回-1
*/
public int getFirstNeigh(int index) {
for (int j = 0; j < vertexList.size(); j++) {
if (str[index][j] > 0) {
return j;
}
}
return -1;
}
// 根據前一個鄰接結點的下標來獲取下一個鄰接結點
public int getNextNeigh(int v1, int v2) {
for (int j = v2 + 1; j < vertexList.size(); j++) {
if (str[v1][j] > 0) {
return j;
}
}
return -1;
}
// 深度優先算法
public void dfs(boolean[] isNut, int i) {
// 首先我們訪問該結點,輸出
System.out.print(getValueByIndex(i) + "->");
// 將結點設置爲已經訪問
isNut[i] = true;
// 查找結點i的第一個鄰接結點w
int w = getFirstNeigh(i);
while (w != -1) {// 說明找到了
if (!isNut[w]) {
dfs(isNut, w);
}
// 如果w結點已經被訪問過
w = getNextNeigh(i, w);
}
}
// 對DFS進行重載,遍歷所有的結點,並進行DFS
public void dfs() {
isNut = new boolean[vertexList.size()];
// 遍歷所有的結點,進行dfs[回溯]
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isNut[i]) {
dfs(isNut, i);
}
}
}
// 廣度優先算法
public void bfs(boolean[] isNut, int i) {
int u; // 表示隊列的頭結點對應下標
int w; // 鄰接結點w
// 隊列,記錄結點訪問的順序
LinkedList<Integer> queue = new LinkedList<Integer>();
// 訪問結點,輸出結點信息
System.out.print(getValueByIndex(i) + "=>");
// 標記爲已訪問
isNut[i] = true;
// 將結點加入隊列
queue.addLast(i);
while (!queue.isEmpty()) {
// 取出隊列的頭結點下標
u = (Integer) queue.removeFirst();
// 得到第一個鄰接結點的下標 w
w = getFirstNeigh(u);
while (w != -1) {// 找到了
// 是否訪問過
if (!isNut[w]) {
System.out.print(getValueByIndex(w) + "=>");
// 標記已經訪問
isNut[w] = true;
// 入隊
queue.addLast(w);
}
// 以u爲前驅點,找w後面的下一個鄰結點
w = getNextNeigh(u, w); // 體現廣度優先
}
}
}
// 對BFS進行重載,遍歷所有的結點,並進行BFS
public void bfs() {
isNut = new boolean[vertexList.size()];
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isNut[i]) {
bfs(isNut, i);
}
}
}
}