Java程式設計:圖

KaiSarH發表於2020-10-09

為什麼要有圖

  1. 前面我們學了線性表和樹
  2. 線性表侷限於一個直接前驅和一個直接後繼的關係
  3. 樹也只能有一個直接前驅也就是父節點
  4. 當我們需要表示多對多的關係時, 這裡我們就用到了圖

圖的舉例說明

圖是一種資料結構,其中結點可以具有零個或多個相鄰元素。兩個結點之間的連線稱為邊。 結點也可以稱為頂點。如圖:
在這裡插入圖片描述

圖的常用概念

  1. 頂點(vertex)
  2. 邊(edge)
  3. 路徑
  4. 無向圖
  5. 有向圖
  6. 帶權圖
    在這裡插入圖片描述
    在這裡插入圖片描述

圖的表示方式

圖的表示方式有兩種:二維陣列表示(鄰接矩陣);連結串列表示(鄰接表)。

  • 鄰接矩陣
    鄰接矩陣是表示圖形中頂點之間相鄰關係的矩陣,對於n個頂點的圖而言,矩陣是的row和col表示的是1…n個點。
    在這裡插入圖片描述
  • 鄰接表
  1. 鄰接矩陣需要為每個頂點都分配n個邊的空間,其實有很多邊都是不存在,會造成空間的一定損失.
  2. 鄰接表的實現只關心存在的邊,不關心不存在的邊。因此沒有空間浪費,鄰接表由陣列+連結串列組成
    在這裡插入圖片描述

圖的快速入門案例

要求: 程式碼實現如下圖結構.

在這裡插入圖片描述

思路分析 (1) 儲存頂點String 使用 ArrayList (2) 儲存矩陣 int[][] edges

程式碼實現

/**
*插入結點(頂點)
*
* @param vertex 結點
*/
public void insertVertex(String vertex) {
   vertexList.add(vertex);
}

/**
* 新增邊
*
* @param v1     表示點的下標 即是第幾個頂點 "A" - "B" => "A" - 0 "B" - 1
* @param v2     第二個頂點對應的下標
* @param weight 表示兩個點是否有關聯
*/
public void insertEdge(int v1, int v2, int weight) {
   edges[v1][v2] = weight;
   edges[v2][v1] = weight;
   numOfEdges++;
}

圖的深度優先遍歷介紹

圖遍歷介紹

所謂圖的遍歷,即是對結點的訪問。一個圖有那麼多個結點,如何遍歷這些結點,需要特定策略,一般有兩種訪問策略:

  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 開始遍歷.

在這裡插入圖片描述

思路分析

在這裡插入圖片描述

程式碼實現

/**
* 對一個結點進行深度優先遍歷演算法
*
* @param isVisited 是否訪問表示矩陣
* @param i         第i個結點 第一次是0
*/
private void dfs(boolean[] isVisited, int i) {
   // 首先訪問該節點
   System.out.print(getValueByIndex(i) + "->");
   // 將該節點設定為已經訪問
   isVisited[i] = true;
   // 查詢結點i的第一個鄰接節點
   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);
       }
   }
}

廣度優先遍歷基本思想

圖的廣度優先搜尋(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。

廣度優先舉例說明

在這裡插入圖片描述

廣度優先程式碼實現

/**
 * 對一個結點進行廣度優先遍歷演算法
 *
 * @param isVisited 是否訪問表示矩陣
 * @param i         第i個結點 第一次是0
 */
private void bfs(boolean[] isVisited, int i) {
    int u;  // 表示佇列頭結點對應下標
    int w;  // 表示鄰接節點w
    // 佇列,記錄結點訪問的順序
    LinkedList<Integer> queue = new LinkedList<>();
    // 訪問結點,輸出資訊
    System.out.print(getValueByIndex(i) + "=>");
    // 標記為已訪問
    isVisited[i] = true;
    // 將節點加入佇列
    queue.addLast(i);
    while (!queue.isEmpty()) {
        // 出去佇列頭結點下標
        u = queue.removeFirst();
        // 得到第一個臨界點下標w
        w = getFirstNeighbor(u);
        while (w != -1) { // 找到了
            // 判斷是否訪問
            if (!isVisited[w]) {
                System.out.print(getValueByIndex(w) + "=>");
                // 標記已訪問
                isVisited[w] = true;
                // 入隊
                queue.addFirst(w);
            }
            // 以u為前驅結點,找w後面的下一個鄰接點
            w = getNextNeighbor(u, w); // 體現出廣度優先
        }
    }
}

// 對dfs過載 遍歷所有結點,並進行dfs
public void bfs() {
    isVisited = new boolean[vertexList.size()];
    for (int i = 0; i < getNumOfVertex(); i++) {
        if (!isVisited[i]) {
            bfs(isVisited, i);
        }
    }
}

應用例項

在這裡插入圖片描述

圖的完整程式碼

package graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;

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[] Vertexes = {"A", "B", "C", "D", "E"};
        String[] Vertexes = {"1", "2", "3", "4", "5", "6", "7", "8"};
        // 建立圖物件
        Graph graph = new Graph(8);
        // 迴圈新增頂點
        for (String Vertex : Vertexes) {
            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.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遍歷是否ok
        System.out.println("深度優先遍歷");
        graph.dfs();
        System.out.println();
        // 測試bfs遍歷是否ok
        System.out.println("廣度優先遍歷");
        graph.bfs();
    }

    /**
     * 構造器
     *
     * @param n 圖的結點數量
     */
    public Graph(int n) {
        // 初始化矩陣和ArrayList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdges = 0;
//        isVisited = new boolean[5];
    }

    /**
     * 插入結點(頂點)
     *
     * @param vertex 結點
     */
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 新增邊
     *
     * @param v1     表示點的下標 即是第幾個頂點 "A" - "B" => "A" - 0 "B" - 1
     * @param v2     第二個頂點對應的下標
     * @param weight 表示兩個點是否有關聯
     */
    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    /**
     * 得到第index個結點的第一個鄰接節點的下標w
     *
     * @param index 想尋求鄰接節點的結點
     * @return 找到返回下標,找不到返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int i = 0; i < vertexList.size(); i++) {
            if (edges[index][i] > 0) {
                return i;
            }
        }
        return -1;
    }


    /**
     * 根據前一個鄰接節點的下標獲取下一個鄰接節點
     *
     * @param v1 結點橫座標
     * @param v2 結點縱座標
     * @return 找到返回下標,找不到返回-1
     */
    public int getNextNeighbor(int v1, int v2) {
        for (int i = v2 + 1; i < vertexList.size(); i++) {
            if (edges[v1][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 對一個結點進行廣度優先遍歷演算法
     *
     * @param isVisited 是否訪問表示矩陣
     * @param i         第i個結點 第一次是0
     */
    private void bfs(boolean[] isVisited, int i) {
        int u;  // 表示佇列頭結點對應下標
        int w;  // 表示鄰接節點w
        // 佇列,記錄結點訪問的順序
        LinkedList<Integer> queue = new LinkedList<>();
        // 訪問結點,輸出資訊
        System.out.print(getValueByIndex(i) + "=>");
        // 標記為已訪問
        isVisited[i] = true;
        // 將節點加入佇列
        queue.addLast(i);
        while (!queue.isEmpty()) {
            // 出去佇列頭結點下標
            u = queue.removeFirst();
            // 得到第一個臨界點下標w
            w = getFirstNeighbor(u);
            while (w != -1) { // 找到了
                // 判斷是否訪問
                if (!isVisited[w]) {
                    System.out.print(getValueByIndex(w) + "=>");
                    // 標記已訪問
                    isVisited[w] = true;
                    // 入隊
                    queue.addFirst(w);
                }
                // 以u為前驅結點,找w後面的下一個鄰接點
                w = getNextNeighbor(u, w); // 體現出廣度優先
            }
        }
    }

    // 對dfs過載 遍歷所有結點,並進行dfs
    public void bfs() {
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                bfs(isVisited, i);
            }
        }
    }

    /**
     * 對一個結點進行深度優先遍歷演算法
     *
     * @param isVisited 是否訪問表示矩陣
     * @param i         第i個結點 第一次是0
     */
    private void dfs(boolean[] isVisited, int i) {
        // 首先訪問該節點
        System.out.print(getValueByIndex(i) + "->");
        // 將該節點設定為已經訪問
        isVisited[i] = true;
        // 查詢結點i的第一個鄰接節點
        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);
            }
        }
    }
    // 圖中常用的方法

    /**
     * 返回結點的個數
     *
     * @return 結點個數
     */
    public int getNumOfVertex() {
        return vertexList.size();
    }

    /**
     * 返回邊的個數
     *
     * @return 邊的個數
     */
    public int getNumOfEdges() {
        return numOfEdges;
    }

    /**
     * 返回結點i(下標) 對應的資料 0->"A" 1->"B" 2->"C"
     *
     * @param i 第i個
     * @return 第i個資料
     */
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    /**
     * 返回兩個結點間的權值
     *
     * @param v1 結點1
     * @param v2 結點2
     * @return 兩個結點間的權值
     */
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }

    /**
     * 顯示圖對應的矩陣
     */
    public void showGraph() {
//        for (int i = 0; i < edges.length; i++) {
//            for (int j = 0; j < edges[0].length; j++) {
//                System.out.print(edges[i][j] + " ");
//            }
//            System.out.println();
//        }
        for (int[] link : edges) {
            System.out.println(Arrays.toString(link));
        }
    }


}

相關文章