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