【數據結構】——圖到底是個什么東西?


不要你覺得,我要我覺得,我說圖它不是個東西。——明人明言。

為什么有圖

用來表示多對多的關系。

線性表局限於一個直接前驅和一個直接后繼的關系

樹也只能有一個直接前驅也就是父節點

基本概念

邊:兩結點的連線

頂點(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用來存儲頂點

二維數組來保存邊信息

常用方法:

  1. 插入頂點
  2. 插入邊
  3. 返回結點的個數
  4. 得到邊的數目,每插入邊就累加一次
  5. 顯示圖對應的矩陣
  6. 返回結點i(下標)對應的數據 0->"A" 1->"B" 2->"C"
  7. 返回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處開始進行遍歷;

算法步驟

  1. 訪問初始結點v,並標記結點v為已訪問。
  2. 查找結點v的第一個鄰接結點w。
  3. 若w存在,則繼續執行4,如果w不存在,則回到第1步,將從v的下一個結點繼續。
  4. 若w未被訪問,對w進行深度優先遍歷遞歸(即把w當做另一個v,然后進行步驟123)。
  5. 查找結點v的w鄰接結點的下一個鄰接結點,轉到步驟3。

步驟詳解:

  1. 初識A的位置為0,標記A,輸出0對應的頂點集合中的A,將A的位置0加到隊列中;
  2. 只要隊列不為空,就取出隊列的頭部,即取出0,找到0對應的下一個結點位置,即就是B的位置為1,
  3. 這里需要判斷B的位置是否存在,如果不為-1就存在,輸出1對應的B元素,標記B,將B加入隊尾;
  4. 如果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);
}
}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM