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