今天博客的內容依然與圖有關,今天博客的主題是關於拓撲排序的。拓撲排序是基於AOV網的,關於AOV網的概念,我想引用下方這句話來介紹:
AOV網:在現代化管理中,人們常用有向圖來描述和分析一項工程的計划和實施過程,一個工程常被分為多個小的子工程,這些子工程被稱為活動(Activity),在有向圖中若以頂點表示活動,有向邊表示活動之間的先后關系,這樣的圖簡稱為AOV網。
說的簡單點,AOV網就是表示一個工程中某些子項的先后順序。就拿工地搬磚來說吧,只有磚廠送來磚,工人才能搬。那么磚廠送磚就是搬磚的前提。先這么一聊,下方會給出詳細的介紹。廢話少說進入今天的主題。
一、AOV網與拓撲排序
本篇博客我們先聊一下AOV網和拓撲排序的關系,下方是我們列舉的一個非常簡單的例子,當然下方的這個圖就是一個簡單的AOV圖,麻雀雖小,五臟俱全。在下方的AOV圖中,送磚和找人是並列的,先執行誰都行。不過搬磚的前提是即送完了磚也找完了人,然后就可以開始搬磚了,所以送磚和找人就是搬磚的前提。那么讓搬磚這件事情順利進行下去的順序有"送磚->找人->搬磚"或者“找人->送磚->搬磚”這兩個序列,而這兩個序列都是拓撲序列。
生成“送磚->找人->搬磚”這個序列的過程我們稱之為拓撲排序。如果非得說的官方和抽象點,那么還是引用拓撲排序的定義吧,下方就是拓撲排序的定義:
拓撲排序:對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊(u,v)∈E(G),則u在線性序列中出現在v之前。通常,這樣的線性序列稱為滿足拓撲次序(Topological Order)的序列,簡稱拓撲序列。簡單的說,由某個集合上的一個偏序得到該集合上的一個全序,這個操作稱之為拓撲排序。
上面這個定義就比較抽象了,當然還是我們搬磚的例子好理解一些。在有向無環圖中的結點如果有入度的話,那么就說明該結點優先級要低於那些可以到達改點的結點。而那些沒有入度的結點的優先級就比較高,這些結點的完成不依賴與其他結點。這樣說如果有些抽象的話,那么我們就看下方拓撲排序詳細的示例圖。
二、拓撲排序示意圖
本部分我們將會給出拓撲排序詳細的示意圖。拓撲排序實現是依賴於棧與隊列的數據結構,棧用來暫存那些入度為0的結點,而隊列負責存儲已經生成的拓撲序列。因為前幾篇關於圖的博客,我們都使用了相同的圖結構。本篇博客也不例外,我們依然會使用之前的有向圖,因為之前的圖是有向無環圖,所以是可以生成拓撲序列的。下方就是要生成拓撲序列的有向無環圖。
在下方的有向無環圖每個結點上有一個綠色的數字,該數字記錄的就是該結點的入度。入度為零,那么該數字就是0,如果入度為1,那么該數字就是1。
下方是下圖拓撲排序每一步的示意圖。接下來我們將會給出下方每一步示意圖的詳細解說。
-
(1):首先將圖中入度為0的結點入棧,因為此圖中只有A結點的入度為0,所以我們將A入棧。
-
(2):將A從棧中Pop到我們的拓撲隊列中,將那些以A發出的邊為入度的結點的度數減一。對於此示例來說也就是將B和F入度減1. 因為B和F的入度數減一后成了0, 所以也將B, F這兩結點入棧。
-
(3):將F從棧中Pop到拓撲隊列中,因為圖中有 F->G和F->E兩條邊,所以 將G和E的入度數減一。因為E的入度數減一后為0,所以將E入棧。
-
(4):將E從棧中Pop到拓撲隊列中,因為圖中E->H和E->D兩條邊, 所以講H和D的兩個結點的入度減一。兩個結點減一操作后沒有入度為零的節點,所以本步沒有結點入棧。
-
(5):接着從棧中Pop結點到拓撲隊列中,將B結點Pop出棧入拓撲隊列。因為C、I、G結點與B相連,B添加進拓撲序列后,這三個結點的入度都減一。C和G的入度減一后為0,所以將其加入棧中。
-
(6):將C從棧中pop到拓撲隊列中,與C相連的結點是I和D, 將這兩個結點的入度減1。I的入度減一后為0,將其Push到棧中暫存。
-
(7):將I結點從棧中pop到拓撲隊列中, D與I相連,將D的入度再次減一。本輪沒有要入棧的結點。
-
(8):將G從棧中pop到拓撲隊列中, G與H和D相連,將D與H的入度減一。H的入度減一操作后入度為零將其入棧。
-
(9):將H從站中pop到拓撲隊列中, D與H相連,將D的入度減一,減一后為0,所以將D入棧。
-
(10):將D從棧中pop到拓撲隊列中,此刻棧中為空,拓撲序列生成完畢。
上面這些步驟已經很詳細了,上面這些步驟搞明白后,給出代碼實現就簡單多了。下方我們會給出具體的代碼實現。
三、拓撲排序的代碼實現
講完概念和原理后,接下來我們就要開始實踐了。本部分就會給出具體的代碼實現,當然我們依然采用Swift語言來做。首先我們創建要依賴的隊列和棧,然后再構建有向圖的鄰接鏈表,最后給出拓撲排序的代碼實現。進入本部分的主題:
1.隊列與棧
接下來我們就要實現拓撲序列生成時要使用的棧與隊列,關於棧與隊列本篇博客就不做過多的贅述了,因為我們之前已經對棧與隊列做了詳細的介紹。關於棧與隊列更詳細的內容請查看之前的博客《棧與隊列的線性和鏈式表示(Swift面向對象版)》。
下方這段代碼段就是我們本篇博客要使用的棧的類,當然是簡化版的,也就是對Array做了一個簡單的封裝。棧中存儲的數據類型是我們鄰接鏈表的結點。具體代碼如下所示。
下方則是我們存儲拓撲序列的隊列,當然也是基於Array的簡單封裝。
2.有向圖的構建
接下來我們來創建我們的有向圖。本篇博客所使用的有向圖我們是使用鄰接鏈表來表示的。下方這段代碼段就是鄰接鏈表的結點,當然在之前不知一篇博客中我們使用到了下方這個結點。本篇博客中的weightNumber不僅僅只存邊的權值,在數組中的結點的weightNumber我們用來存儲該結點的入度。
下方這段代碼就是有向圖的創建,在網鄰接鏈表上掛入結點時,要講被掛入的結點的入度加1即可。因為下方代碼與之前圖的創建的代碼類似,在此就不做過多贅述了。
下方這兩個截圖則是上述代碼段的輸入和輸出。根據輸出的結果我們不難看出我們所創建的圖就是一個有向圖。
3、拓撲序列的生成
接下來就是我們本篇博客代碼實現的核心了。我們將基於上面創建的AOV網來生成拓撲序列。其實下方生成拓撲序列的代碼就是上述示例描述的具體實現。接下來我們將具體的說下下方這段拓撲排序的代碼。主要概括起來分為下方三步:
-
(1):首先初始化我們所需要的棧,然后 遍歷AOV網中所有的結點,將入度為0的結點添加到我們的棧中暫存。
-
(2):循環將我們棧中的元素添加到拓撲隊列中。每從棧中Pop出一個結點就把與該結點相連的結點的入度減1,如果減一后該結點的度數為0則將其入棧。然后繼續下一輪的循環。
-
(3):當棧中沒有暫存的結點后,說明拓撲序列生成完畢。如果拓撲隊列中的元素要小於圖結點的個數,那么說明圖中存在環路,不能生成相應的拓撲序列。
下方截圖就是我們之前創建的有向圖所生成的拓撲序列,如下所示:
至此,我們本篇博客的內容也就結束了,下方依然是我們本篇博客所涉及Demo的分享鏈接,如下所示:
github分享鏈接:https://github.com/lizelu/DataStruct-Swift/tree/master/TopoLogicalSort