基礎-6:圖的表示和遍歷

1 概述

圖是數據結構中最復雜的形式,也是最燒腦的結構。無數的牛人樂此不疲地鉆研,然而,時至今日,依然有很多問題等著童鞋們去解決。為什么圖重要,下面以兩個簡單的例子進行說明:

  • 銀行轉賬超過10000,要24小時后到賬,為什么?本人猜測:因為后臺服務器要計算這筆轉賬是否涉嫌詐騙、洗錢,交易是在一張超級巨大的圖上(每個賬號是圖中一個節點),服務器計算的重點是圖中是否存在某個強連通圖或者某個節點在某個時間點入度特別大等等。因為計算需要時間較長,無法在幾分鐘內算完,所以要求24小時延遲也是正常的;
  • 間友圈價值再發現。沒錯,就是我們現在寫作的平臺,通過間友之間的關注、收藏、閱讀等,能迅速組成一張圖,通過這張圖,可以很快計算出誰是最受歡迎的作者,誰的粉絲最多,誰的文章最受歡迎,誰是通過不正常手段獲取的粉絲等等...

圖計算的地位在最近的機器學習領域日益體現,tensorflow等深度學習框架無一不是基于圖計算的,圖計算中最基礎的當屬圖的表示和圖的遍歷。本文剩余部分介紹圖的表示和兩個最基本的遍歷算法(廣度遍歷、深度遍歷)并對算法本身進行分析說明。(算法實現地址:https://github.com/bjutJohnson/Algorithms/tree/master/src/graph ,請查閱)

2 圖的表示

與其它基本的數據結構不同,圖的表示較為復雜。在圖中,存在兩類元素,一類是節點元素,一類是邊元素。在具體工程實踐中,每個節點可能包括一系列屬性,如過用一個節點表示人,則其屬性可能包括身份證號、年齡、性別等;每條邊也可能包括一系列屬性,針對人,邊的屬性可能包括親戚、朋友、同事等關系。在具體的工程項目中,通常都會給每個節點和邊賦予唯一的ID,在此基礎上進行一系列的計算。

具體而言,圖的表示可以有兩種方法,一種是矩陣表示法,一種是鄰接表法,其對應的例子如下圖所示:

圖1:圖的表示方法

圖1中的b是鄰接表法,c是矩陣表示法。從兩種表示方法的形式上看,當圖中的邊比較稀疏時,用鄰接表法表示更節省空間,但是判斷特定的頂點對是否存在邊,需要搜索鄰接表對應的線性表,比較費事;當圖中的邊比較密集時,用矩陣表示法比較合適,判斷特定的頂點對是否存在邊,直接取值判定即可,效率較高。當前,隨著存儲空間越來越便宜,機器學習深度學習等算法中通常采用矩陣表示方法,用空間換取時間。在本文中,我們一般用鄰接表方法來表示圖。圖的表示的具體代碼分為,圖節點表示、邊表示、圖表示,分別如下所示:

// 定義圖節點文件
type GraphNode struct {
    id    int   // 節點唯一標識
    edges []int // 標識鄰接邊的編號
    color int8  // 標識是否訪問

    feature map[string]interface{} // 定義圖節點本身的屬性,以key-value的形式提供,用戶負責轉換,另一種方案是使用reflect
}
// 定義圖上的邊
type GraphEdge struct {
    id       int                    // 唯一標識符
    from     int                    // 出節點
    to       int                    // 入節點
    features map[string]interface{} // 邊上的各種屬性,暫時可能用不著
}
// 管理圖的所有節點
type GraphManager struct {
    id2Node map[int]*GraphNode // 以map的形式記錄所有的節點
    id2Edge map[int]*GraphEdge // 以map的形式記錄所有的邊

    nodeCounter int // 計數器,申請的圖節點數量
    edgeCounter int // 邊計數器

    isDirection bool // 有向圖還是無向圖,必須初始時給定

    lockChan chan bool // 控制對管理對象的操作,一個時刻只能有一個goroutine對其進行修改

    logicTime int // 用于控制深度遍歷時的邏輯時鐘,只表示先后關系,如t0=0, t1=1表示t1是緊鄰t0的下一個時刻
}

3 圖的遍歷

與樹結構中存在先序遍歷、中序遍歷、和后序遍歷一樣,在圖中存在廣度遍歷和深度遍歷兩種方式,這兩種方式幾乎是所有圖計算的最基礎部分,其重要性不言而喻。

3.1 廣度遍歷

廣度遍歷的概念比較容易理解,其語義為:針對當前節點v,先遍歷所有t的所有未訪問的鄰接節點{v1,v,v2, ... vm},然后再以此以{v1,v,v2, ... vm}中的元素為當前節點,遞歸進行,直至所有的節點都遍歷完畢。下面的圖可能更容易表示廣度遍歷的過程:

圖2:廣度遍歷圖示,摘自算法導論

圖2中是以s點為源節點開始進行廣度遍歷,初始時,所有的節點涂成白色,遍歷過程中,當第一次發現節點時,將節點涂灰,當節點的所有鄰接節點都發現后,將節點涂黑,圖中節點類的數據表示節點與源節點之間的最短距離(這個需要分析說明)。

試證明:廣度遍歷的結果是各節點與源節點的最短距離

證明(數學歸納法),假設求節點v與源節點s的最短距離
1)當v與s相鄰時,其最短距離必然為1,廣度遍歷得到的值顯然為1,結論成立;
2)當v與s相距k時,假設廣度遍歷得到的值最短距離為k;
3)當v與s相距k+1時,必然存在一個節點v'與v距離為k,v'與s相鄰,根據廣度遍歷算法過程,在訪問到v'之后,必然再下一輪會訪問到v,因此得到的必然是k+1;

如果覺得上述分析比較啰嗦,可以觀察圖2,廣度遍歷就是逐層往外擴散遍歷的過程,得到的是一顆廣度優先樹,如下圖所示:


圖3: 廣度優先樹

根據上面的進程,廣度遍歷的算法實現思路如下:


摘自算法導論

(具體的實現請詳見 https://github.com/bjutJohnson/Algorithms/blob/master/src/graph/graph_manager.go 中的BFS函數,可能實現與上面的算法有些不一致。)算法借助一個隊列,首先,將源節點s入隊列,然后,進入循環,在隊列不為空的前提下,從隊列中取出一個元素v(11行),然后訪問其所有未訪問的鄰接節點(12-17行)將其涂灰,并將未訪問的節點入隊列,當v的所有鄰接節點訪問完成之后,將自身涂黑。

節點為白色,表示節點未被發現,節點為灰色時,表示節點已被防線,且已被加入到隊列中,當節點為黑色時,表示節點已出列,并且其所有鄰接點已被發現。從算法中,可以看出一個節點均只有一次涂白,一次涂灰和一次涂黑的機會。

其時間復雜度直接分析遍歷的過程,有N個節點、M條邊,訪問節點必然是O(M),通過連接表發現相鄰節點必然是O(N),因此,其時間復雜度必然為O(M+N)

3.2 深度遍歷

與廣度遍歷對應最短路徑不同,深度遍歷更多地用于拓撲排序、強連通分量發現等。深度遍歷的語義是:總是對最近發現的節點v出發的邊進行探索,如果v的所有邊都已探索完畢,則回溯至其上層節點p(v是p的鄰接節點,由p發現v),進一步探索p的其它未遍歷的鄰接節點,若p的所有鄰接節點已探索完畢,則回溯至p的上層節點繼續探索,直至所有的節點都探索完畢。

說起來也是很啰嗦,看如下圖示:


圖4: 深度遍歷圖示,摘自算法導論

圖中節點標識的m/n,其中m表示節點發現的邏輯時間,n表示其所有的鄰接節點都被訪問的時間,簡單描述如下:

  • (a)首先u被發現,u涂成灰色;
  • (b)探索u的鄰接節點,發現了v,v涂成灰色;
  • (c)進一步探索v的鄰接節點,發現了y,y涂成灰色;
  • (d)探索y的鄰接節點,發現了x,x涂成灰色;
  • (e)嘗試探索x的鄰接節點y,發現y已發現;
  • (f)x的所有的鄰接節點都已訪問,回溯至y;
  • (g)y的所有鄰接點都已訪問,回溯至v;
  • ...

其對應的算法為:

深度優先算法,摘自算法導論

(注:具體的實現算法請見:https://github.com/bjutJohnson/Algorithms/blob/master/src/graph/graph_manager.go 中的DFS函數)。同樣其時間復雜度為O(M+N)。

下面對節點的發現時間和鄰接節點都被發現的時間進行分析(對于后面的拓撲排序、強連通分量非常重要)。假設深度遍歷過程中得到的節點相關時間為:x(m1/n1), y(m2/n2),不妨取m2 > m1,則以下二者必然有一個成立:

  • m1 < m2, n1 > n2
  • n1 < m2

證明:
1)若x是y的上層節點(或距離更遠的上層節點),根據深度優先遍歷算法,m1 < m2肯定成立,當y的所有鄰接點都訪問結束時(此時才對n2賦當前邏輯時間值),然后,一直向上層回溯至x,x遍歷完所有的鄰接節點才會結束(此時才對n1賦當前邏輯時間值),根據時間先后的描述,顯然有:m1 < m2, n1 > n2;如圖4中的節點u, v, y, x
2)若x不是y的上層節點,則必然是通過x無法訪問到y,要訪問y,必然是通過算法中的第5行,從其中選擇的y,在此之前,x的上層節點和下層節點,必然都已經訪問完畢,而y還未被訪問,因此,必然有n1 < m2,如圖4中的u和w

通常情況下,深度優先算法中的第5行中選擇一個節點開始遍歷時,對應此節點的一棵深度優先樹(這更多的是從后續使用的角度來說明,對于有向樹來說,容易理解,對于無向樹而言,必然是一顆樹),因此,通常將有向圖深度優先遍歷的結果表示成一個森林(因為是多棵樹)。如下圖所示:

圖5: 有向樹的深度遍歷

對于圖中的邊,根據與優先遍歷生成樹的邊之間的關系,圖中的邊可以分為:

  • 樹邊:深度優先圖過程中,生成的樹中的邊,如圖5(c)中的涂陰影的邊均為樹邊;
  • 后向邊:如圖5(c)中標識為B的邊,其產生的原因是,在深度遍歷中,x是y的上層節點,而在訪問y時,發現其有指向x的邊,這一類邊稱為后向邊;
  • 前向邊:如圖5(c)中標識為F的邊,其產生的原因是:在深度遍歷中,x是y和z的上層節點,y是z的上層節點,在發現x后,優先訪問y,通過y又訪問到z,則(x, y)和(y, z)是樹邊,但是(x, z)則不是樹邊,這種邊稱為前向邊;
  • 橫向邊:不是上述3類的邊稱為橫向邊。

在理解了上述定義,可以得出如下結論:

試證明:在對無向圖G進行深度優先搜索時,每條邊要么是樹邊,要么是后向邊。

證明:前面已經分析過,如果無向圖中沒有孤立節點或孤立節點集合(那樣是多個圖,而非一個圖),即從一個節點出發,必然可以深度遍歷訪問所有的節點(也就是說算法中的第5行中的循環只進入一次),在訪問過程中,如果訪問節點x時,通過x的鄰接表發現y,則y必然是樹邊;如果訪問節點x時,如果x鄰接表為空,則開始回溯,從x的上層節點到x的邊必為樹邊,如果x的鄰接表非空,但其鄰接節點y已訪問,則y必然是x的上層節點(有可能有幾層),因此,從x到y的邊必然為后向邊。不可能存在其它情況,因此,結論成立。

4 小結

本文講解了圖的基本表示和兩個基本的遍歷算法,其中也伴有一些簡單的分析。圖是最考驗思考能力的一種形式,其對應的算法也最難,后續課程將會針對圖進一步分析講解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,165評論 0 12
  • https://zh.visualgo.net/graphds 淺談圖形結構https://zh.visualgo...
    狼之獨步閱讀 4,230評論 0 0
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,821評論 0 19
  • 現實生活中有很大一類問題可以用簡潔明了的圖論語言來描述,可以轉化為圖論問題。 相關定義 圖可以表示為G=(V, E...
    芥丶未央閱讀 1,763評論 0 7
  • 愿你我都能有這樣一只籃子,時常滌蕩,直到成為我們生命中,無法割舍的一部分。 故事說的是在20世紀初,山東平原縣一戶...
    逯素娟閱讀 932評論 0 1