博客 / 詳情

返回

數據結構-圖

概述

圖是一種較為複雜的非線性結構。 為啥説其較為複雜呢?

根據前面的內容,我們知道:

  • 線性數據結構的元素滿足唯一的線性關係,每個元素(除第一個和最後一個外)只有一個直接前趨和一個直接後繼。
  • 樹形數據結構的元素之間有着明顯的層次關係。

但是,圖形結構的元素之間的關係是任意的。

何為圖呢? 簡單來説,圖就是由頂點的有窮非空集合和頂點之間的邊組成的集合。通常表示為:G(V,E),其中,G 表示一個圖,V 表示頂點的集合,E 表示邊的集合。

下圖所展示的就是圖這種數據結構

同時圖⼜分為有向圖與⽆向圖,上⾯的是⽆向圖,因為邊沒有指明⽅向,只是表示兩者關聯關係,⽽有向圖則是這樣:

圖在我們日常生活中的例子很多!比如我們在社交軟件上好友關係就可以用圖來表示。

圖的基本概念

頂點

圖中的數據元素,我們稱之為頂點,圖至少有一個頂點(非空有窮集合)

對應到好友關係圖,每一個用户就代表一個頂點。

頂點之間的關係用邊表示。

對應到好友關係圖,兩個用户是好友的話,那兩者之間就存在一條邊。

度表示一個頂點包含多少條邊,在有向圖中,還分為出度和入度,出度表示從該頂點出去的邊的條數,入度表示進入該頂點的邊的條數。

對應到好友關係圖,度就代表了某個人的好友數量。

無向圖和有向圖

邊表示的是頂點之間的關係,有的關係是雙向的,比如同學關係,A 是 B 的同學,那麼 B 也肯定是 A 的同學,那麼在表示 A 和 B 的關係時,就不用關注方向,用不帶箭頭的邊表示,這樣的圖就是無向圖。

有的關係是有方向的,比如父子關係,師生關係,微博的關注關係,A 是 B 的爸爸,但 B 肯定不是 A 的爸爸,A 關注 B,B 不一定關注 A。在這種情況下,我們就用帶箭頭的邊表示二者的關係,這樣的圖就是有向圖。

無權圖和帶權圖

對於一個關係,如果我們只關心關係的有無,而不關心關係有多強,那麼就可以用無權圖表示二者的關係。

對於一個關係,如果我們既關心關係的有無,也關心關係的強度,比如描述地圖上兩個城市的關係,需要用到距離,那麼就用帶權圖來表示,帶權圖中的每一條邊一個數值表示權值,代表關係的強度。

下圖就是一個帶權有向圖。

圖的存儲

鄰接矩陣存儲

鄰接矩陣將圖用二維矩陣存儲,是一種較為直觀的表示方式。

如果第 i 個頂點和第 j 個頂點之間有關係,且關係權值為 n,則 A[i][j]=n

在無向圖中,我們只關心關係的有無,所以當頂點 i 和頂點 j 有關係時,A[i][j]=1,當頂點 i 和頂點 j 沒有關係時,A[i][j]=0。如下圖所示:

值得注意的是:無向圖的鄰接矩陣是一個對稱矩陣,因為在無向圖中,頂點 i 和頂點 j 有關係,則頂點 j 和頂點 i 必有關係。

鄰接矩陣存儲的方式優點是簡單直接(直接使用一個二維數組即可),並且,在獲取兩個定點之間的關係的時候也非常高效(直接獲取指定位置的數組元素的值即可)。但是,這種存儲方式的缺點也比較明顯,那就是比較浪費空間,

鄰接表存儲

針對上面鄰接矩陣比較浪費內存空間的問題,誕生了圖的另外一種存儲方法—鄰接表

鄰接鏈表使用一個鏈表來存儲某個頂點的所有後繼相鄰頂點。對於圖中每個頂點 Vi,把所有鄰接於 Vi 的頂點 Vj 鏈成一個單鏈表,這個單鏈表稱為頂點 Vi 的 鄰接表。如下圖所示:

大家可以數一數鄰接表中所存儲的元素的個數以及圖中邊的條數,你會發現:

  • 在無向圖中,鄰接表元素個數等於邊的條數的兩倍,如左圖所示的無向圖中,邊的條數為 7,鄰接表存儲的元素個數為 14。
  • 在有向圖中,鄰接表元素個數等於邊的條數,如右圖所示的有向圖中,邊的條數為 8,鄰接表存儲的元素個數為 8。

圖的搜索

圖⾥⾯遍歷⼀般分為⼴度優先遍歷和深度優先遍歷,⼴度優先遍歷是指優先遍歷與當前頂點直接相關的頂點,⼀般藉助隊列實現。⽽深度優先遍歷則是往⼀個⽅向⼀直⾛到不能再⾛,有點不撞南牆不回頭的意思,⼀般使⽤遞歸實現。

廣度優先搜索

廣度優先搜索就像水面上的波紋一樣一層一層向外擴展,如下圖所示:

廣度優先搜索的具體實現方式用到了之前所學過的線性數據結構——隊列 。具體過程如下圖所示:

第 1 步:

第 2 步:

第 3 步:

第 4 步:

第 5 步:

第 6 步:

深度優先搜索

深度優先搜索就是“一條路走到黑”,從源頂點開始,一直走到沒有後繼節點,才回溯到上一頂點,然後繼續“一條路走到黑”,如下圖所示:

和廣度優先搜索類似,深度優先搜索的具體實現用到了另一種線性數據結構——棧 。具體過程如下圖所示:

第 1 步:

第 2 步:

第 3 步:

第 4 步:

第 5 步:

第 6 步:

算法框架如下:

// 記錄被遍歷過的節點
boolean[] visited;
// 記錄從起點到當前節點的路徑
boolean[] onPath;

/* 圖遍歷框架 */
void traverse(Graph graph, int s) {
    if (visited[s]) return;
    // 經過節點 s,標記為已遍歷
    visited[s] = true;
    // 做選擇:標記節點 s 在路徑上
    onPath[s] = true;
    for (int neighbor : graph.neighbors(s)) {
        traverse(graph, neighbor);
    }
    // 撤銷選擇:節點 s 離開路徑
    onPath[s] = false;
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.