1. 程式人生 > >資料結構基礎溫故-5.圖(中):圖的遍歷演算法

資料結構基礎溫故-5.圖(中):圖的遍歷演算法

上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用一個foreach語句遍歷存放頂點資訊的陣列即可。但是,如果為了實現特定演算法,就必須要根據邊的資訊按照一定的順序進行遍歷。圖的遍歷演算法是求解圖的連通性問題、拓撲排序和求解關鍵路徑等演算法的基礎。

一、圖的遍歷

  圖的遍歷要比樹的遍歷複雜得多,由於圖的任一頂點都可能和其餘頂點相鄰接,所以在訪問了某頂點之後,可能順著某條邊又訪問到了已訪問過的頂點。因此,在圖的遍歷過程中,必須記下每個訪問過的頂點,以免同一個頂點被訪問多次。為此,給頂點附加一個訪問標誌isVisited,其初值為false,一旦某個頂點被訪問,則將其isVisited標誌設為true。

        protected class Vertex<TValue>
        {
            public TValue data;     // 資料
            public Node firstEdge;  // 鄰接點連結串列頭指標
            public bool isVisited;  // 訪問標誌:遍歷時使用

            public Vertex()
            {
                this.data = default(TValue);
            }

            
public Vertex(TValue value) { this.data = value; } }

  在上面的頂點類的定義中,增加了一個bool型別的成員isVisited,用於在遍歷時判斷是否已經訪問過了該頂點。一般在進行遍歷操作時,會首先將所有頂點的isVisited屬性置為false,於是可以寫一個輔助方法InitVisited(),如下所示:

        /// <summary>
        /// 輔助方法:初始化頂點的visited標誌為false
        
/// </summary> private void InitVisited() { foreach (Vertex<T> v in items) { v.isVisited = false; } }

  圖的遍歷方法主要有兩種:一種是深度優先搜尋遍歷(Depth-First Search,DFS),另一種是廣度優先搜尋遍歷(Breadth-First Search,BFS)。下面,我們就來仔細看看這兩種圖的遍歷演算法。

二、深度優先搜尋遍歷

2.1 深度優先遍歷原理

  圖的深度優先遍歷類似於二叉樹的深度優先遍歷,其基本思想是:從圖中某個頂點v出發,訪問此頂點,然後從v的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有路徑相通的頂點都被訪問到。顯然,這是一個遞迴的搜尋過程。

  以上圖為例,假定V1是出發點,首先訪問V1。這時兩個鄰接點V2、V3均未被訪問,可以選擇V2作為新的出發點,訪問V2之後,再找到V2的未訪問過的鄰接點。同V2鄰接的有V1、V4和V5,其中V1已經訪問過了,可以選擇V4作為新的出發點。重複上述搜尋過程,繼續依次訪問V8、V5。訪問V5之後,由於與V5相鄰的頂點均已被訪問過,搜尋退回到V8,訪問V8的另一個鄰接點V6.接下來依次訪問V3和V7,最後得到的訪問序列為V1→V2→V4→V8→V5→V6→V3→V7。

2.2 深度優先遍歷實現

  (1)實現程式碼

        /// <summary>
        /// 深度優先遍歷介面For連通圖
        /// </summary>
        public void DFSTraverse()
        {
            InitVisited(); // 首先初始化visited標誌
            DFS(items[0]); // 從第一個頂點開始遍歷
        }

        /// <summary>
        /// 深度優先遍歷演算法
        /// </summary>
        /// <param name="v">頂點</param>
        private void DFS(Vertex<T> v)
        {
            v.isVisited = true; // 首先將訪問標誌設為true標識為已訪問
            Console.Write(v.data.ToString() + " "); // 進行訪問操作:這裡是輸出頂點data
            Node node = v.firstEdge;

            while (node != null)
            {
                if (node.adjvex.isVisited == false) // 如果鄰接頂點未被訪問
                {
                    DFS(node.adjvex); // 遞迴訪問node的鄰接頂點
                }
                node = node.next; // 訪問下一個鄰接點
            }
        }

  深度優先遍歷是一個典型的遞迴過程,這裡也使用了遞迴的方式。

  (2)遍歷測試

  這裡的測試程式碼構造的圖如下所示:

  測試程式碼如下所示:

        static void MyAdjacencyListDFSTraverseTest()
        {
            Console.Write("深度優先遍歷:");
            MyAdjacencyList<string> adjList = new MyAdjacencyList<string>();
            // 新增頂點
            adjList.AddVertex("V1");
            adjList.AddVertex("V2");
            adjList.AddVertex("V3");
            adjList.AddVertex("V4");
            adjList.AddVertex("V5");
            adjList.AddVertex("V6");
            adjList.AddVertex("V7");
            adjList.AddVertex("V8");
            // 新增邊
            adjList.AddEdge("V1", "V2");
            adjList.AddEdge("V1", "V3");
            adjList.AddEdge("V2", "V4");
            adjList.AddEdge("V2", "V5");
            adjList.AddEdge("V3", "V6");
            adjList.AddEdge("V3", "V7");
            adjList.AddEdge("V4", "V8");
            adjList.AddEdge("V5", "V8");
            adjList.AddEdge("V6", "V8");
            adjList.AddEdge("V7", "V8");
            // DFS遍歷
            adjList.DFSTraverse();
            Console.WriteLine();
        }
View Code

  執行結果如下圖所示:

三、廣度優先搜尋遍歷

3.1 廣度優先遍歷原理

  圖的廣度優先遍歷演算法是一個分層遍歷的過程,和二叉樹的廣度優先遍歷類似,其基本思想在於:從圖中的某一個頂點Vi觸發,訪問此頂點後,依次訪問Vi的各個為層訪問過的鄰接點,然後分別從這些鄰接點出發,直至圖中所有頂點都被訪問到

  對於上圖所示的無向連通圖,若從頂點V1開始,則廣度優先遍歷的頂點訪問順序是V1→V2→V3→V4→V5→V6→V7→V8。

3.2 廣度優先遍歷實現

  (1)實現程式碼

        /// <summary>
        /// 寬度優先遍歷介面For連通圖
        /// </summary>
        public void BFSTraverse()
        {
            InitVisited(); // 首先初始化visited標誌
            BFS(items[0]); // 從第一個頂點開始遍歷
        }

        /// <summary>
        /// 寬度優先遍歷演算法
        /// </summary>
        /// <param name="v">頂點</param>
        private void BFS(Vertex<T> v)
        {
            v.isVisited = true; // 首先將訪問標誌設為true標識為已訪問
            Console.Write(v.data.ToString() + " "); // 進行訪問操作:這裡是輸出頂點data
            Queue<Vertex<T>> verQueue = new Queue<Vertex<T>>(); // 使用佇列儲存
            verQueue.Enqueue(v);

            while (verQueue.Count > 0)
            {
                Vertex<T> w = verQueue.Dequeue();
                Node node = w.firstEdge;
                // 訪問此頂點的所有鄰接節點
                while (node != null)
                {
                    // 如果鄰接節點沒有被訪問過則訪問它的邊
                    if (node.adjvex.isVisited == false)
                    {
                        node.adjvex.isVisited = true; // 設定為已訪問
                        Console.Write(node.adjvex.data + " "); // 訪問
                        verQueue.Enqueue(node.adjvex); // 入隊
                    }
                    node = node.next; // 訪問下一個鄰接點
                }
            }
        }

  和樹的層次遍歷類似,藉助了佇列這一資料結構進行輔助,記錄頂點的鄰接點。

  (2)遍歷測試

  這裡構造的圖如下所示,跟上面原理中的圖一致:

  測試程式碼如下所示:

        static void MyAdjacencyListTraverseTest()
        {
            MyAdjacencyList<string> adjList = new MyAdjacencyList<string>();
            // 新增頂點
            adjList.AddVertex("V1");
            adjList.AddVertex("V2");
            adjList.AddVertex("V3");
            adjList.AddVertex("V4");
            adjList.AddVertex("V5");
            adjList.AddVertex("V6");
            adjList.AddVertex("V7");
            adjList.AddVertex("V8");
            // 新增邊
            adjList.AddEdge("V1", "V2");
            adjList.AddEdge("V1", "V3");
            adjList.AddEdge("V2", "V4");
            adjList.AddEdge("V2", "V5");
            adjList.AddEdge("V3", "V6");
            adjList.AddEdge("V3", "V7");
            adjList.AddEdge("V4", "V8");
            adjList.AddEdge("V5", "V8");
            adjList.AddEdge("V6", "V8");
            adjList.AddEdge("V7", "V8");

            Console.Write("廣度優先遍歷:");
            // BFS遍歷
            adjList.BFSTraverse();
            Console.WriteLine();
        }
View Code

  執行結果如下圖所示:

四、非連通圖的遍歷

以上討論的圖的兩種遍歷方法都是針對無向連通圖的,它們都是從一個頂點觸發就能訪問到圖中的所有頂點。若無方向圖是非連通圖,則只能訪問到初始點所在連通分量中的所有頂點,其他分量中的頂點是無法訪問到的。如下圖所示,V6、V7以及V8三個頂點均訪問不到。為此,需要從其他每個連通分量中選擇初始點,分別進行遍歷,才能夠訪問到圖中的所有頂點。

4.1 非連通圖的深度優先遍歷實現

        /// <summary>
        /// 深度優先遍歷介面For非聯通圖
        /// </summary>
        public void DFSTraverse4NUG()
        {
            InitVisited();
            foreach (var v in items)
            {
                if (v.isVisited == false)
                {
                    DFS(v);
                }
            }
        }

  這裡DFS方法跟上面無向連通圖的保持一致。

4.2 非連通圖的廣度優先遍歷實現

        /// <summary>
        /// 廣度優先遍歷介面For非聯通圖
        /// </summary>
        public void BFSTraverse4NUG()
        {
            InitVisited();
            foreach (var v in items)
            {
                if (v.isVisited == false)
                {
                    BFS(v);
                }
            }
        }

  這裡BFS方法跟上面無向連通圖的保持一致。

4.3 非連通圖的遍歷測試

  構造的圖如上圖所示,測試程式碼如下:

        static void MyAdjacencyListTraverseTest()
        {
            Console.WriteLine("------------非連通圖的遍歷------------");
            MyAdjacencyList<string> numAdjList = new MyAdjacencyList<string>();
            // 新增頂點
            numAdjList.AddVertex("V1");
            numAdjList.AddVertex("V2");
            numAdjList.AddVertex("V3");
            numAdjList.AddVertex("V4");
            numAdjList.AddVertex("V5");
            numAdjList.AddVertex("V6");
            numAdjList.AddVertex("V7");
            numAdjList.AddVertex("V8");
            // 新增邊
            numAdjList.AddEdge("V1", "V2");
            numAdjList.AddEdge("V1", "V4");
            numAdjList.AddEdge("V2", "V3");
            numAdjList.AddEdge("V2", "V5");
            numAdjList.AddEdge("V4", "V5");
            numAdjList.AddEdge("V6", "V7");
            numAdjList.AddEdge("V6", "V8");
            Console.Write("深度優先遍歷:");
            // DFS遍歷
            numAdjList.DFSTraverse4NUG();
            Console.WriteLine();
            Console.Write("廣度優先遍歷:");
            // BFS遍歷
            numAdjList.BFSTraverse4NUG();
        }
View Code

  執行結果如下圖所示:

附件下載

參考資料

(1)程傑,《大話資料結構》

(2)陳廣,《資料結構(C#語言描述)》

(3)段恩澤,《資料結構(C#語言版)》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

資料結構基礎-6.查詢基本查詢與樹表查詢

只要你開啟電腦,就會涉及到查詢技術。如炒股軟體中查股票資訊、硬碟檔案中找照片、在光碟中搜DVD,甚至玩遊戲時在記憶體中查詢攻擊力、魅力值等資料修改用來作弊等,都要涉及到查詢。當然,在網際網路上查詢資訊就更加是家常便飯。查詢是計算機應用中最常用的操作之一,也是許多程式中最耗時的一部分,查詢方法的優劣對於系統的執

資料結構基礎-6.查詢雜湊表

雜湊(雜湊)技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖示表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。雜湊技術最適合的求解問題是查詢與給定值相等的記錄。

資料結構基礎-5.演算法

上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用

資料結構基礎-5.最小生成樹演算法

圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成樹 1.1 生成樹   對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過

資料結構基礎-5.的基本概念

前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的關係。圖(Graph)是一種複雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關係是任意的。現實生活中的很多

資料結構基礎-5.最短路徑

圖的最重要的應用之一就是在交通運輸和通訊網路中尋找最短路徑。例如在交通網路中經常會遇到這樣的問題:兩地之間是否有公路可通;在有多條公路可通的情況下,哪一條路徑是最短的等等。這就是帶權圖中求最短路徑的問題,此時路徑的長度不再是路徑上邊的數目總和,而是路徑上的邊所帶權值的和。帶權圖分為無向帶權圖和有向帶權圖,但如

資料結構基礎最小生成樹演算法

轉自:http://www.cnblogs.com/edisonchou/p/4681602.html   圖(中):最小生成樹演算法 圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成

資料結構基礎演算法

轉自:http://www.cnblogs.com/edisonchou/p/4676876.html   圖(中):圖的遍歷演算法 上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問

資料結構基礎-1.線性表

在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。 一、單鏈表基礎 1.1 單鏈表的

資料結構基礎-4.樹與二叉樹

上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過

資料結構基礎-4.樹與二叉樹

在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系

資料結構基礎-4.樹與二叉樹

前面所討論的線性表元素之間都是一對一的關係,今天我們所看到的結構各元素之間卻是一對多的關係。樹在計算機中有著廣泛的應用,甚至在計算機的日常使用中,也可以看到樹形結構的身影,如下圖所示的Windows資源管理器和應用程式的選單都屬於樹形結構。樹形結構是一種典型的非線性結構,除了用於表示相鄰關係外,還可以表示層次

資料結構基礎-1.線性表

在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。 一、迴圈連結串列基礎 1.1 迴圈連結串列節點結構   迴圈連結串列和單鏈表的

資料結構基礎的基本概念

轉自:http://www.cnblogs.com/edisonchou/p/4672188.html   圖(上):圖的基本概念 前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的

資料結構基礎-2.棧

現實生活中的事情往往都能總結歸納成一定的資料結構,例如餐館中餐盤的堆疊和使用,羽毛球筒裡裝的羽毛球等都是典型的棧結構。而在.NET中,值型別線上程棧上進行分配,引用型別在託管堆上進行分配,本文所說的“棧”正是這種資料結構。棧和佇列都是常用的資料結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制

資料結構基礎-3.佇列

在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如

資料結構基礎-7.排序

排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為按關鍵字“有序”的記錄序列。如何進行排序,特別是高效率地進行排序時計算機工作者學習和研究的重要課題之一。排序有內部排序和外部排序之分,若整個排序過程不需要訪問外存便能完成,則稱此類排序為內部排序,反之則為外部排序。本篇主

Go語言基礎—— Go語言結構體、Go語言切片Slice、range、Go語言Map集合

Go語言結構體 Go 語言中陣列可以儲存同一型別的資料,但在結構體中我們可以為不同項定義不同的資料型別。 結構體是由一系列具有相同型別或不同型別的資料構成的資料集合。  結構體表示一項記錄,比如儲存圖書館的書籍記錄,每本書有以下屬性: Title :標題&nbs

基於順序儲存實現的多叉樹7深度

 1    template<typename T> 2    template<bool is_const,bool is_reverse> 3    inline typename mtree<T,false>::template fd_iterator_impl<

基於順序儲存實現的多叉樹6葉子

 1    template<typename T> 2    template<bool is_const,bool is_reverse> 3    inline typename mtree<T,false>::template leaf_iterator_impl&