這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本科學習資料結構的時候就聽前輩說過,學好資料結構、電腦群組成、作業系統和電腦網路後就會成為大神,我也使勁學過,但是一直沒探索資料結構的用處。實際編程用過比較多的就是雜湊表了,一般語言也都會通過一些擴充包支援。
《數學之美》第九章——《圖論和網路爬蟲》,就淺顯易懂的介紹了圖的實際用途。搜尋引擎裡面的網路爬蟲抓取網路資料,就是把互連網抽象成有向圖這種資料結構,通過遍曆這張圖實現的互連網抓取。
圖一般分為有向圖和無向圖,一般用來開發網路爬蟲和地圖(我就知道這兩個)。圖可以認為是節點和串連邊的集合,有兩種實現方式:鄰接表和鄰接矩陣。稀疏圖用鄰接表實現,稠密圖用鄰接矩陣實現。圖的重點在於遍曆,有深度優先遍曆和廣度優先遍曆。深度優先遍曆可以通過遞迴實現,而廣度優先遍曆要轉換成類似於樹的層序遍曆來實現。還要注意一點,圖的遍曆要防止迴路,否則無法結束遍曆。
type Graph struct {edgnum intvexnum intadj []Vertex}type Vertex struct {Data stringe *Edge}type Edge struct {ivex intnext *Edge}
定義了三個結構體, Graph
是圖的資料結構,它包含邊數、頂點數和頂點的集合;Vertex
是頂點資料結構,包含頂點的資料內容和邊的頭指標,Edge
邊的結構體定義了與此頂點相連的另一個頂點在圖的頂點集合中的位置和下一個邊的地址。
圖是頂點和邊的集合,adj
對象就是頂點的集合,每一個頂點內部的e
就是通過鏈表實現的邊的集合。這也就實現了圖。
在圖中插入頂點,這個比較簡單,加入頂點的集合即可:
func (this *Graph) InsertVertex(v Vertex) {this.adj = append(this.adj, v)}
在圖中插入邊,需要指出相連的兩個頂點的資訊,然後把邊的資訊插入到鏈表的最後:
func (this *Graph) InsertEdge(v1, v2 Vertex) {var p *Edge = this.adj[this.get_position(v1.Data)].eif p == nil {// fmt.Println("nil...", v1.Data, v2.Data)this.adj[this.get_position(v1.Data)].e = NewEdge(this.get_position(v2.Data))} else {for ; p.next != nil; p = p.next {}p.next = NewEdge(this.get_position(v2.Data))// fmt.Println("append...", v1.Data, v2.Data)}}
擷取某個頂點的鄰接頂點,找到頂點的邊集合,就可以通過邊集合找到與它相連的頂點了。也可以通過這個方法計算得到頂點的出度(Out Degree):
func (this *Graph) Adjacent(v Vertex) []Vertex {res := make([]Vertex, 0)p := this.adj[this.get_position(v.Data)].efor ; p != nil; p = p.next {res = append(res, this.adj[p.ivex])}return res}func (this *Graph) OutDegree(v Vertex) int {res := 0p := this.adj[this.get_position(v.Data)].efor p != nil {res++p = p.next}return res}
計算頂點的入度相比出度有點麻煩,因為圖本身儲存的邊的集合是邊的起始位置,結束位置,也就是入度,只能遍曆整個邊集合來計算:
func (this *Graph) InDegree(v Vertex) int {res := 0pos := this.get_position(v.Data)for _, a := range this.adj {p := a.efor p != nil {if pos == p.ivex {res++}p = p.next}}return res}
廣度優先遍曆,這個遍曆類似於層序遍曆,每遍曆一層,需要記住當前層的節點,然後與遍曆當前層相連的節點,如此實現遍曆。需要一個隊列來記住當前層,先進先出。我是通過Golang的slice
實現的簡單隊列。還有一個問題,就是需要防止迴路,也就是說,一個節點不能遍曆兩次。這裡用了Golang內建的map
實現,隨機儲存、隨機尋找。遍曆還要防止非完全圖的情況,如果只有結點沒有邊還需要處理,所以這裡用for
迴圈來處理:
func (this *Graph) Bfs() {res := map[int]Vertex{}for _, a := range this.adj {Q := []Vertex{a}for len(Q) != 0 {u := Q[0]Q = Q[1:]if _, ok := res[this.get_position(u.Data)]; !ok {Q = append(Q, this.Adjacent(u)...)res[this.get_position(u.Data)] = ufmt.Printf("%s ", u.Data)}}}fmt.Printf("\n")}
深度優先遍曆,遍曆完一個節點,接著遍曆這個節點的這個節點相連的節點,直到結束。用遞迴非常簡單:
func (this *Graph) Dfs() {res := map[int]Vertex{}for _, a := range this.adj {this.dfs(a, res)}fmt.Printf("\n")}func (this *Graph) dfs(u Vertex, res map[int]Vertex) {if _, ok := res[this.get_position(u.Data)]; !ok {res[this.get_position(u.Data)] = ufmt.Printf("%s ", u.Data)p := u.efor p != nil {if _, ok := res[p.ivex]; !ok {this.dfs(this.adj[p.ivex], res)}p = p.next}}}
完整的測試代碼,大Golang的單元測試工具,直接go test
即可:
func TestMain(t *testing.T) {g := create_example_lgraph()g.Print()println("Bfs.............")g.Bfs()println("Dfs.............")g.Dfs()if g.InDegree(Vertex{Data: "E"}) != 2 {t.Error("indegree of E wanted 2 bug got", g.InDegree(Vertex{Data: "E"}))}if g.OutDegree(Vertex{Data: "E"}) != 2 {t.Error("outdegree of E wanted 2 bug got", g.OutDegree(Vertex{Data: "E"}))}}func create_example_lgraph() *Graph {vexs := []string{"A", "B", "C", "D", "E", "F", "G"}edges := [][2]string{{"A", "B"},{"B", "C"},{"B", "E"},{"B", "F"},{"C", "E"},{"D", "C"},{"E", "B"},{"E", "D"},{"F", "G"}}vlen := len(vexs)elen := len(edges)pG := Create()// 初始化"頂點數"和"邊數"pG.vexnum = vlenpG.edgnum = elen// 初始化"鄰接表"的頂點for i := 0; i < pG.vexnum; i++ {pG.InsertVertex(*NewVertex(vexs[i]))}// 初始化"鄰接表"的邊for i := 0; i < pG.edgnum; i++ {// 讀取邊的起始頂點和結束頂點pG.InsertEdge(*NewVertex(edges[i][0]), *NewVertex(edges[i][1]))}return pG}
本文所涉及到的完整源碼請參考。
######參考文獻+ 【1】wangkuiwu/datastructs_and_algorithm - GitHub+ 【2】《資料結構與演算法——C語言描述》,Mark Allen Weiss著+ 【3】《演算法導論》Thomas H. Cormen等著+ 【4】《數學之美》吳軍著
原文連結:Golang通過鄰接表實現有向圖,轉載請註明來源!