[數據結構與算法] 圖結構

介紹

在之前的學習中, 我們學了線性結構(數組, 鏈表,棧和隊列)非線性結構中的樹結構. 下面就讓我們學習非線性結構中的圖結構吧

圖出現的原因

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

圖的舉例

圖是一種非線性的數據結構,其中結點可以具有零個或多個相鄰元素。兩個結點之間的連接稱爲邊。 結點也可以稱爲頂點。 如下圖:

在這裏插入圖片描述

常用概念

  1. 頂點(vertex):
    圖中的節點
  2. 邊(edge):
    圖中相鄰節點的連接
  3. 路徑:
    圖中任意兩個節點間連接的組合
  4. 無向圖:
    頂點間連接無方向
  5. 有向圖
    頂點間連接無方向
  6. 帶權圖
    頂點間連接有方向

無向圖
在這裏插入圖片描述
有向圖
在這裏插入圖片描述

圖的表示方式有兩種:二維數組表示(鄰接矩陣);鏈表表示(鄰接表)。

鄰接矩陣

鄰接矩陣是表示圖形中頂點之間相鄰關係的矩陣,對於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();
    }

結果驗證

在這裏插入圖片描述

深度優先與廣度優先比較
在這裏插入圖片描述

由上圖可知

  1. 圖的深度優先遍歷, 先找到一個節點, 然後以這個點爲出發點去尋找下一層的點
  2. 圖的廣度優先遍歷, 先找到一個節點, 然後以該點爲基礎訪問該層節點.(一層一層的訪問)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章