算法筆記(五)圖的廣度優先遍歷和深度優先遍歷

你對圖的理解是什麼?

     你是否經常聽到這句話,在兩個開發之間交流時常說 “有紙麼?畫個圖看看”,可見圖在我們的日常生活、工作中發揮的巨大作用,對於圖的理解還有很多場景,都是來自於生活 如電視劇中的藏寶圖爲得到而羣雄爭霸、名人名畫也稱爲圖,綜上所述圖更明確的含義是帶有某種信息的畫叫做圖,圖可以比文字表達更多的信息,語言表達、文字遠遠沒有一幅圖生動形象具體,這也是爲什麼很多人用圖來展示自己的想法。

爲什麼有圖這種數據結構?

     大家都使用過數組,數組中的元素都是按着物理地址空間串連起來的,像一條線一樣,每個元素只有一個直接前驅和直接後驅,等到了樹形結構呢,數據之間具有了明顯的層次關係並且每一層上的數據元素可能會下一層多個節點之間存在關係,且只和上一層一個節點有關係,這有點像一對多不過這種一對多是有方向的,如同我們的家譜中一對雙親可能有多個孩子。

     現實生活是複雜多變的,拿和我們息息相關的高鐵來說,每一列車連通了兩個或多個城市的連接,每個城市和其它各個城市都可能連接,這是錯綜複雜的關係,非線性也非層次關係,然而這種複雜關係可以用圖來形象的表示,說到這裏你是否理解了爲什麼會有圖這種數據結構呢?

     說的更通俗一些圖這種數據結構是因爲它可以表達數據之間更爲複雜的邏輯關係,這種數據間的邏輯關係是數組、鏈表不能表達或者表達不方便的關係,給解決特定問題或編寫程序帶來了很大的便利,它的產生是現實生活中特定問題催化出來的,如同java 設計模式一樣,設計模式是對於類之間關係抽象總結昇華的結果。以後隨着計算機運算速度飛速提高、量子計算機的發展,也可能會出現新的數據結構來應對。

定義

  • 標準定義:圖(Graph)是由頂點(Vertex)的有窮集合和頂點之間的邊(Edge)的集合組成,通常表示爲:G(V,E),其中G表示一個圖,V 是圖G中頂點的集合,E是圖G中邊的集合。
  • 無向圖:任意兩個頂點之間的邊都是無向的。
  • 有向圖:任意兩個頂點之間的邊都是有向的。
  • 簡單圖:無連接自身的邊並且任意兩個頂點之間只有一條邊
  • 無向完全圖:任意兩個頂點之間都存在一條邊
  • 有向完全圖:任意兩個頂點之間都存在一條有向邊
  • 稀疏圖:邊少的圖
  • 稠密圖:邊多的圖
  • 權:邊有值得圖
  • 子圖:頂點和邊包含在圖中
  • 連通圖:任意一點v存在到達頂點v’的路徑

如下面圖所示,表達了上面概念的關係,畫圖容易記憶:
這裏寫圖片描述

創建、存儲

     我們已經對圖結構的定義有了解,主要是由頂點和邊組成,存儲圖的問題也就是對頂點和邊的存儲了,那麼我們如何來存儲這些頂點和邊呢?

     頂點在圖中不僅僅只頂點,還有頂點之間邊的關係,頂點可以使用數組存儲,對於邊總共有n的平方個邊,即完全有向圖最少一個邊都沒有,由此邊的範圍是[0-n^2]即存儲,常見的兩種方法是使用鄰接矩陣和鄰接鏈表形式,除了這兩種還有很多種表示方法,在這裏不一一列出,我們用無向完全圖來舉例:
這裏寫圖片描述
     如上圖我們來創建一個具有5個頂點,且每個頂點都和其它4個頂點有邊的無向完全圖。

     該圖共有5個頂點各個頂點之間並沒有訪問先後順序,所以沒有寫標出頂點編號,各個頂點他們是一樣的通過創建一個實體類來表示這個圖的存儲結構:

實體類

     該實體類有屬性主要有兩個屬性一維數組來存儲圖的各個頂點、二維數組來存儲頂點之間的表關係,該實體類可以表示一個圖的存儲,這個思路主要就是用一維數組和二維數組來存儲頂點和邊關係,當然除了利用數組存儲,還有很多其它方式 如鏈表等


/**
 * Created by lilongsheng on 2017/8/2.
 * 圖結構存儲實體類
 */
public class GraphEntity {

    /*
        頂點數 默認值
     */
    public static int MAX_VEX = 5 ;
    /*
        存儲頂點的一維數組
     */
    public String[] vexs = new String[MAX_VEX];
    /*
        鄰接矩陣 表示頂點之間的表關係 二維數組
     */
    public String[][] arc = new String[MAX_VEX][MAX_VEX];
    /*
        圖中當前節點的頂點數 、邊數
     */
    private int numVertexes;//圖中當前的頂點數
    private int numEdges;//圖中當前邊數

    public String[] getVexs() {
        return vexs;
    }

    public void setVexs(String[] vexs) {
        this.vexs = vexs;
    }

    public String[][] getArc() {
        return arc;
    }

    public void setArc(String[][] arc) {
        this.arc = arc;
    }

    public int getNumVertexes() {
        return numVertexes;
    }

    public void setNumVertexes(int numVertexes) {
        this.numVertexes = numVertexes;
    }

    public int getNumEdges() {
        return numEdges;
    }

    public void setNumEdges(int numEdges) {
        this.numEdges = numEdges;
    }
}

圖的操作

  1. 新增頂點
  2. 刪除頂點
  3. 返回圖中指定頂點的座標位置
  4. 返回圖中指定頂點的值
  5. 圖的深度遍歷
  6. 圖的廣度遍歷
  7. ………………等等

     由圖的操作大家是否會想到數組、隊列等操作,可以對比一下,其它他們的操作都是類似的,都是結合自身結構特點對數據的操作,只不過表現形式不同而已。

     同樣是數據,在線性表中叫元素,在樹中叫節點,在圖中叫頂點;對於關係,在線性表中相鄰元素之間有線性關係,樹中相鄰兩層節點間有層次關係,圖中任意兩頂點之間可能有關係;

兩種遍歷算法

深度遍歷

/**
     * 鄰接矩陣的深度遍歷操作
     * @param G
     */
    public void DFSTraverse(GraphEntity G) {
        int i;
        for (i = 0; i < G.getNumVertexes(); i++) {
            visited[i] = false; //初始化所有頂點狀態爲未訪問過
        }

        for (i = 0; i < G.getNumVertexes(); i++) {
            //對未訪問過的頂點調用DFS,若是連通圖只會執行一次
            if (!visited[i]) {
                DFS(G, i);
            }
        }
    }

    /**
     * 深度遍歷未訪問的頂點
     * @param G
     * @param i
     */
    public void DFS(GraphEntity G, int i) {
        int j;
        visited[i] = true;
        //打印頂點信息
        for (j = 0; j < G.getNumVertexes(); j++) {
            if (G.arc[i][j] == "1" && !visited[j]) {
                DFS(G, j);//對未訪問的鄰接頂點遞歸調用
            }
        }
    }

廣度遍歷

/**
 * Created by lilongsheng on 2017/8/2.
 * 圖的廣度遍歷
 */
public class BreadthFirstSearchTraverse {

    boolean[] visited = new boolean[GraphEntity.MAX_VEX];

    public void BFSTraverse(GraphEntity G){

        int i , j;
        Queue queue;
        //將圖的所有頂點標記爲未訪問
        for (i = 0; i < G.getNumVertexes(); i++){
            visited[i] = false;
        }

        //對每一個頂點做循環
        for (i = 0; i < G.getNumVertexes(); i++){

            //若是未訪問過的頂點就處理
            if (!visited[i]){

                visited[i] = true;//設置當前頂點訪問過
                System.out.println("頂點{"+i+"}");
                //將此頂點入隊
                enterQueue(i);
                //若當前隊列不爲空
                while (!queryEmptyQueue()){

                    //元素出隊列,遍歷該出隊元素的所有沒有訪問過的鄰接頂點
                    deleteQueue(i);

                    for (j = 0; j < G.getNumVertexes(); j++){
                        //判斷其它頂點與該頂點存在邊且未訪問過
                        if (G.arc[i][j] == "1" && !visited[j]){

                            visited[j] = true;//將找到的頂點標記爲已訪問
                            System.out.println("頂點{"+ j +"}");

                            deleteQueue(i);//將找到的頂點入隊

                        }
                    }
                }
            }
        }
    }

與網絡爬蟲關係

     我們可以把互聯網上的網頁看成一個節點,連接各個網頁的超鏈接看做邊,有了超鏈接我們可以從任何一個節點出發利用圖遍歷算法訪問每一個網頁,這是最簡單的“網絡爬蟲”,互聯網上的網頁數量級都在億級別,爲提高效率一般用利用散列表來存儲哪些網頁下載過,一個商業的網絡爬蟲由成千上萬個服務器組成。
     這裏面有幾個問題需要思考,選擇哪一個遍歷算法好些?網站頻繁建立連接?瀏覽器內核工程師解析內頁與url?好爬蟲需要在有限的時間裏最多的爬下最重要的內容。

廣度遍歷問題

第一個循環的作用?

     對於代碼裏面的每行需要知道它的意思我們才能夠寫出來代碼,剛開始看了代碼不太理解,有第一個循環是因爲圖並不一定是連通圖,可能它的一部分並沒有連通這樣如果從一個頂點開始,其它頂點就會有遺漏,爲了充分遍歷到每一個頂點,我們首先寫了一個循環,該循環對於連通圖其實是循環一次裏面,我們會通過visited數組來判斷該頂點是否被訪問過,如果訪問過,不會再執行後面的邏輯。

廣度遍歷的原則是如何把握?

     既然是廣度遍歷那麼在遍歷圖的時候就需要遵從這個原則,代碼是怎麼控制訪問順序符合這一原則的,我覺得主要是通過兩點來達到這個目的,第一個是隊列先進先出的這種性格;第二個是當前訪問頂點的所有未訪問鄰接頂點訪問完時,終止改成循環,從隊列取出元素繼續循環;這兩個條件保證拿出來上一層某個節點A時,和A同一層的B節點的未訪問鄰接頂點已經都入隊且在後面,出來的元素都是A層同層節點。

總結

     圖這種數據結構應用範圍很廣泛,在各種算法設計中也是基本的數據結構,它的重要性在解決某些重要問題的時候會顯示出來重要作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章