在 Go 中對依賴圖進(jìn)行排序
最近,我在思考在軟件工程中遇到的許多重要問題可以歸結(jié)為幾個(gè)簡單的問題。只要看看任何關(guān)于算法的書,其中的大部分都會(huì)是排序或搜索集合的一些變體。谷歌的存在是因?yàn)椤澳男┪臋n包含這些短語?”是一個(gè)真正難以解決的問題(好吧,這極大地簡化了 Google 產(chǎn)品的龐大范圍,但基本思想仍然成立)。
01 什么是拓?fù)渑判颍?/span>
在我的職業(yè)生涯中,我一次又一次遇到的常見問題之一就是對依賴圖的節(jié)點(diǎn)進(jìn)行拓?fù)渑判?。換句話說,給定一些有向無環(huán)圖 — 想想可以依賴于其他軟件包或大型公司項(xiàng)目中的任務(wù)的軟件包 — 對它們進(jìn)行排序,以便列表中的任何項(xiàng)目都不會(huì)依賴于列表中后面出現(xiàn)的任何內(nèi)容。假設(shè)我們正在制作蛋糕,在開始之前,我們需要一些原料。讓我們來簡化一下,說我們只需要雞蛋和面粉。嗯,要吃雞蛋,我們需要雞(相信我,我在這里不是開玩笑),要吃面粉,我們需要谷物。雞也需要谷物作為飼料,谷物需要土壤和水才能生長。我們考慮表達(dá)所有這些依賴關(guān)系的圖表:

該圖的一種可能的拓?fù)漤樞蚴牵?/p>
[]string{"soil",?"water",?"grain",?"chickens",?"flour",?"eggs",?"cake"}
但是,還有其他可能的拓?fù)漤樞颍?/p>
[]string{"water",?"soil",?"grain",?"flour",?"chickens",?"eggs",?"cake"}
我們也可以把面粉放在雞蛋后面,因?yàn)槲ㄒ灰蕾囯u蛋的就是蛋糕。由于我們可以重新排列項(xiàng)目,我們還可以并行完成其中一些項(xiàng)目,同時(shí)保持任何項(xiàng)目都不會(huì)出現(xiàn)在依賴于它的任何東西之前。例如,通過添加一層嵌套,我們可以表明內(nèi)部切片中的所有內(nèi)容都獨(dú)立于該切片中的其他任何內(nèi)容:
[][]string{
????{"soil",?"water"},
????{"grain"},
????{"chickens",?"flour"},
????{"eggs"},
????{"cake"},
}
從這個(gè)圖中,我們得到了一個(gè)很好的“執(zhí)行計(jì)劃”,用于為蛋糕準(zhǔn)備依賴項(xiàng)。首先,我們需要找到一些土壤和水。接下來,我們種植谷物。然后,我們同時(shí)養(yǎng)一些雞和做面粉,收集雞蛋。最后,我們可以做蛋糕了!對于小于四歲的人來說,這似乎是一項(xiàng)艱巨的工作,但好的事情需要時(shí)間。
02 構(gòu)建依賴圖
現(xiàn)在我們了解了要做什么,讓我們考慮如何編寫一些能夠構(gòu)建這種依賴項(xiàng)列表的代碼。我們當(dāng)然需要跟蹤元素本身,我們需要跟蹤什么取決于什么。為了使“取決于什么X?” 和“X取決于什么?”兩者都高效,我們將跟蹤兩個(gè)方向的依賴關(guān)系。
我們已經(jīng)足夠了解開始編寫代碼所需的內(nèi)容:
//?A?node?in?this?graph?is?just?a?string,?so?a?nodeset?is?a?map?whose
//?keys?are?the?nodes?that?are?present.
type?nodeset?map[string]struct{}
//?depmap?tracks?the?nodes?that?have?some?dependency?relationship?to
//?some?other?node,?represented?by?the?key?of?the?map.
type?depmap?map[string]nodeset
type?Graph?struct?{
?nodes?nodeset
?//?Maintain?dependency?relationships?in?both?directions.?These
?//?data?structures?are?the?edges?of?the?graph.
?//?`dependencies`?tracks?child?->?parents.
?dependencies?depmap
?//?`dependents`?tracks?parent?->?children.
?dependents?depmap
?//?Keep?track?of?the?nodes?of?the?graph?themselves.
}
func?New()?*Graph?{
?return?&Graph{
??dependencies:?make(depmap),
??dependents:???make(depmap),
??nodes:????????make(nodeset),
?}
}
這種數(shù)據(jù)結(jié)構(gòu)應(yīng)該適合我們的目的,因?yàn)樗覀冃枰乃行畔ⅲ汗?jié)點(diǎn)、“依賴”邊和“依賴于”邊?,F(xiàn)在讓我們考慮創(chuàng)建用于向圖形添加新依賴關(guān)系的 API。所有我們需要的是一個(gè)聲明的方法,一些節(jié)點(diǎn)依賴于另一個(gè),就像這樣:graph.DependOn("flour", "grain")。有幾種情況我們要明確禁止。首先,一個(gè)節(jié)點(diǎn)不能依賴于自己,其次,如果flour依賴于grain,那么grain一定不能依賴于flour,否則我們會(huì)創(chuàng)建一個(gè)無限的依賴循環(huán)。有了這個(gè),讓我們編寫Graph.DependOn()方法。
func?(g?*Graph)?DependOn(child,?parent?string)?error?{
?if?child?==?parent?{
??return?errors.New("self-referential?dependencies?not?allowed")
?}
?//?The?Graph.DependsOn()?method?doesn't?exist?yet.
?//?We'll?write?it?next.
?if?g.DependsOn(parent,?child)?{
??return?errors.New("circular?dependencies?not?allowed")
?}
?//?Add?nodes.
?g.nodes[parent]?=?struct{}{}
?g.nodes[child]?=?struct{}{}
?//?Add?edges.
?addNodeToNodeset(g.dependents,?parent,?child)
?addNodeToNodeset(g.dependencies,?child,?parent)
?return?nil
}
func?addNodeToNodeset(dm?depmap,?key,?node?string)?{
?nodes,?ok?:=?dm[key]
?if?!ok?{
??nodes?=?make(nodeset)
??dm[key]?=?nodes
?}
?nodes[node]?=?struct{}{}
}
一旦我們實(shí)現(xiàn),這將有效地為我們的圖表添加依賴關(guān)系Graph.DependsOn()。我們可以很容易地判斷一個(gè)節(jié)點(diǎn)是否直接依賴于其他某個(gè)節(jié)點(diǎn),但我們也想知道是否存在傳遞依賴。例如,由于flour依賴于grain并且grain依賴于soil,因此也flour依賴于soil。這將要求我們獲取節(jié)點(diǎn)的直接依賴項(xiàng),然后對于這些依賴項(xiàng)中的每一個(gè),獲取其依賴項(xiàng)等等,直到我們停止發(fā)現(xiàn)新的依賴項(xiàng)。用計(jì)算機(jī)科學(xué)術(shù)語來說,我們正在計(jì)算一個(gè)固定點(diǎn),以在我們的圖上找到“DependsOn”關(guān)系的傳遞閉包。
func?(g?*Graph)?DependsOn(child,?parent?string)?bool?{
?deps?:=?g.Dependencies(child)
?_,?ok?:=?deps[parent]
?return?ok
}
func?(g?*Graph)?Dependencies(child?string)?nodeset?{
?if?_,?ok?:=?g.nodes[root];?!ok?{
??return?nil
?}
?
?out?:=?make(nodeset)
?searchNext?:=?[]string{root}
?for?len(searchNext)?>?0?{
??//?List?of?new?nodes?from?this?layer?of?the?dependency?graph.?This?is
??//?assigned?to?`searchNext`?at?the?end?of?the?outer?"discovery"?loop.
??discovered?:=?[]string{}
??for?_,?node?:=?range?searchNext?{
???//?For?each?node?to?discover,?find?the?next?nodes.
???for?nextNode?:=?range?nextFn(node)?{
????//?If?we?have?not?seen?the?node?before,?add?it?to?the?output?as?well
????//?as?the?list?of?nodes?to?traverse?in?the?next?iteration.
????if?_,?ok?:=?out[nextNode];?!ok?{
?????out[nextNode]?=?struct{}{}
?????discovered?=?append(discovered,?nextNode)
????}
???}
??}
??searchNext?=?discovered
?}
?
?return?out
}
03 對圖表進(jìn)行排序
現(xiàn)在我們有了一個(gè)圖數(shù)據(jù)結(jié)構(gòu),可以考慮如何按照拓?fù)漤樞驅(qū)⒐?jié)點(diǎn)取出。如果我們可以發(fā)現(xiàn)葉節(jié)點(diǎn)—即,節(jié)點(diǎn)本身對其他節(jié)點(diǎn)沒有依賴關(guān)系—那么我們可以重復(fù)獲取葉子并將它們從圖中移除,直到圖為空。在第一次迭代中,我們將找到獨(dú)立的元素,然后在隨后的每次迭代中,我們將找到僅依賴于已刪除元素的節(jié)點(diǎn)。最終結(jié)果將是一個(gè)按拓?fù)渑判虻莫?dú)立“層”節(jié)點(diǎn)的切片。
獲取圖的葉子很簡單。我們只需要找到在 dependencies 中沒有條目的節(jié)點(diǎn)。這意味著它們不依賴于任何其他節(jié)點(diǎn)。
func?(g?*Graph)?Leaves()?[]string?{
?leaves?:=?make([]string,?0)
?for?node?:=?range?g.nodes?{
??if?_,?ok?:=?g.dependencies[node];?!ok?{
???leaves?=?append(leaves,?node)
??}
?}
?return?leaves
}
最后一塊拼圖實(shí)際上是計(jì)算圖的拓?fù)渑判驅(qū)印_@也是最復(fù)雜的一塊。我們將遵循的一般策略是迭代地收集葉子并將它們從圖中刪除,直到圖為空。由于我們將對圖進(jìn)行變異,因此我們希望對其進(jìn)行克隆,以便在執(zhí)行排序后原始圖仍然完好無損,因此我們將繼續(xù)實(shí)施該克?。?/p>
func?copyNodeset(s?nodeset)?nodeset?{
?out?:=?make(nodeset,?len(s))
?for?k,?v?:=?range?s?{
??out[k]?=?v
?}
?return?out
}
func?copyDepmap(m?depmap)?depmap?{
?out?:=?make(depmap,?len(m))
?for?k,?v?:=?range?m?{
??out[k]?=?copyNodeset(v)
?}
?return?out
}
func?(g?*Graph)?clone()?*Graph?{
?return?&Graph{
??dependencies:?copyDepmap(g.dependencies),
??dependents:???copyDepmap(g.dependents),
??nodes:????????copyNodeset(g.nodes),
?}
}
我們還需要能夠從圖中刪除一個(gè)節(jié)點(diǎn)和所有邊。刪除節(jié)點(diǎn)很簡單,就像從每個(gè)節(jié)點(diǎn)刪除出站邊一樣。然而,我們跟蹤兩個(gè)方向的每條邊的事實(shí)意味著我們必須做一些額外的工作來刪除入站記錄。我們將用于刪除所有邊的策略如下:
在 dependents中查找節(jié)點(diǎn)A的條目。這為我們提供了依賴于A的節(jié)點(diǎn)集 。對于這些節(jié)點(diǎn)中的每一個(gè),在 dependencies中找到條目。從nodeset中刪除A。在 dependents中刪除節(jié)點(diǎn)A的條目。執(zhí)行逆操作,在 dependencies中查找節(jié)點(diǎn)A等。
借助一個(gè)允許我們從 depmap 條目中刪除節(jié)點(diǎn)的小實(shí)用程序,我們可以編寫從圖中完全刪除節(jié)點(diǎn)的方法。
func?removeFromDepmap(dm?depmap,?key,?node?string)?{
?nodes?:=?dm[key]
?if?len(nodes)?==?1?{
??//?The?only?element?in?the?nodeset?must?be?`node`,?so?we
??//?can?delete?the?entry?entirely.
??delete(dm,?key)
?}?else?{
??//?Otherwise,?remove?the?single?node?from?the?nodeset.
??delete(nodes,?node)
?}
}
func?(g?*Graph)?remove(node?string)?{
?//?Remove?edges?from?things?that?depend?on?`node`.
?for?dependent?:=?range?g.dependents[node]?{
??removeFromDepmap(g.dependencies,?dependent,?node)
?}
?delete(g.dependents,?node)
?//?Remove?all?edges?from?node?to?the?things?it?depends?on.
?for?dependency?:=?range?g.dependencies[node]?{
??removeFromDepmap(g.dependents,?dependency,?node)
?}
?delete(g.dependencies,?node)
?//?Finally,?remove?the?node?itself.
?delete(g.nodes,?node)
}
最后,我們可以實(shí)現(xiàn) Graph.TopoSortedLayers():
func?(g?*Graph)?TopoSortedLayers()?[][]string?{
?layers?:=?[][]string{}
?//?Copy?the?graph
?shrinkingGraph?:=?g.clone()
?for?{
??leaves?:=?shrinkingGraph.Leaves()
??if?len(leaves)?==?0?{
???break
??}
??layers?=?append(layers,?leaves)
??for?_,?leafNode?:=?range?leaves?{
???shrinkingGraph.remove(leafNode)
??}
?}
?return?layers
}
這種方法清楚地概述了我們對圖進(jìn)行拓?fù)渑判虻牟呗裕?/p>
克隆圖,以便我們可以對其進(jìn)行轉(zhuǎn)變。 反復(fù)將圖的葉子收集到輸出的“層”中。 收集后刪除每一層。 當(dāng)圖為空時(shí),返回收集的圖層。
現(xiàn)在我們可以回到最初的蛋糕制作問題,以確保我們的圖為我們解決了這個(gè)問題:
package?main
import?(
?"fmt"
?"strings"
?"github.com/kendru/darwin/go/depgraph"
)
func?main()?{
?g?:=?depgraph.New()
?g.DependOn("cake",?"eggs")
?g.DependOn("cake",?"flour")
?g.DependOn("eggs",?"chickens")
?g.DependOn("flour",?"grain")
?g.DependOn("chickens",?"grain")
?g.DependOn("grain",?"soil")
?g.DependOn("grain",?"water")
?g.DependOn("chickens",?"water")
?for?i,?layer?:=?range?g.TopoSortedLayers()?{
??fmt.Printf("%d:?%s\n",?i,?strings.Join(layer,?",?"))
?}
?//?Output:
?//?0:?soil,?water
?//?1:?grain
?//?2:?flour,?chickens
?//?3:?eggs
?//?4:?cake
}
所有這些工作都不是小菜一碟,但現(xiàn)在我們有了一個(gè)依賴圖,可以用來對幾乎任何東西進(jìn)行拓?fù)渑判?。您可?span style="font-weight: bold;color: rgb(60, 112, 198);">在 GitHub 上找到[1]這篇文章的完整代碼。這個(gè)實(shí)現(xiàn)有一些明顯的限制,我想挑戰(zhàn)你改進(jìn)它,以便它可以:
存儲(chǔ)不是簡單字符串的節(jié)點(diǎn) 允許單獨(dú)添加節(jié)點(diǎn)和邊/依賴信息 產(chǎn)生用于調(diào)試的字符串輸出
原文鏈接:https://kendru.github.io/go/2021/10/26/sorting-a-dependency-graph-in-go/
參考資料
在 GitHub 上找到: https://github.com/kendru/darwin/tree/main/go/depgraph
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
