
這天國民女兒知世在合唱團練唱時,不幸遭受「圖」Golang牌攻擊,導致無法發出聲音。因此尋求怪醫黑捷克開刀,但怪醫黑捷克一口價三千萬,就算知世家在有錢也請不起。

各位魔法使們能一起幫助小櫻收服「圖」Golang牌嗎?
當小櫻朋友真滴很衰吔,不是被冰住(第1季第33集),就是沒聲音(第2季第2集),要不然就是遊樂園失火(第1季第35集),甚至是摔下懸崖(第1季第25集 和 第1季第34集)
簡單來說,圖就是樹的進化版,圖的邊可以比樹跟樹的鍵結更隨易,幾乎沒有限制,可以有向可以無向,可以兩點間不只一條邊,甚至還可以在邊上設權重。
圖主要由兩個構造所構成分別為「點」和「邊」,其實跟樹非常像。「樹」可以說是圖的特例
記錄一張圖通常有兩種方式:Adjacency matrix, Adjacency list 前者比較簡單但很耗空間,由二維陣列(或切片)構成,後者比較抽象一點,是由陣列(或切片)配合鏈結列所構成。
因為這兩個名詞一般都是講英文,大家直接記得英文就好,中文就不用了

| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 | 1 | 1 | ||||
| 1 | ||||||
| 2 | 1 | 1 | ||||
| 3 | 1 | 1 | ||||
| 4 | 1 | 1 | ||||
| 5 | 
以最左邊的欄代表起點,最上邊的列代表終點,如果有一條邊符合就記錄為 1,沒有就記錄為 0,因為 0 太多了所以省略不填。
圖沒有規定兩個點間只有一條邊如果兩個點間有兩條邊,則可以記錄為 2,依此類推
可以很明顯地看出這個二維陣列有值的部分並不多,如果用 Adjacency matrix 記錄更大張的圖,那麼會浪費更多空間。因此一般會推薦採用 Adjacency list


以左邊的陣列{0,1,2,3,4,5}為起始點,linked list 上的點為終點,形成 Ajacency list

首先我們先創建一個 package,至於要怎麼創建?請複習:#14 套件 Package (2020) & 套件實戰(math, http, sort) | Golang魔法使
./graph/graph.go
package graph
import (
    "container/list"
    "fmt"
)
type Graph struct{
    // 建立一個鏈結串列切片
    adj_list [](*list.List)
}
type Vertex struct{
    Val int
}
NewGraph() 和 NewVertex()
其中,在使用 NewGraph() 時告訴有幾個點
利用 NewVertex() 創立點
func NewGraph(number_of_node int) *Graph{
    g := new(Graph)
    g.adj_list = make([](*list.List), number_of_node)
    for k, _ := range g.adj_list{
        g.adj_list[k] = list.New()
    }
    return g
}
func NewVertex(number int) *Vertex{
    v := new(Vertex)
    v.Val = number
    return v
}
AddEdge()
允許使用者分別給定起始點和結束點建立一條邊
這部分很簡單,就是把「結束點」插在 Graph 的 adj_list[啟始點] 的前或後
func (g *Graph) AddEdge(start, end *Vertex){
    // list 包中的 PushBack() 可以在原有的串列後增加新的元素
    g.adj_list[start.Val].PushBack(end)
}
EasyTraversal()
因為建立完後看不到長怎樣,所以我們再簡單的把圖表中的資料印出來看一看
這種走訪沒什麼實際意義,只是方便我們 debug 一下
func (g *Graph) EasyTraversal(){
    // adj_list[index] 內存的是 *list.List
    // 利用 .Front() 可以取得頭點
    // 利用 .Next() 可以取得下個點
    // 在 list 包中節點的型態為 Element
    // 而利用 .Value 可以取得原先放進去的值該值為 interface{} (泛型)
    for k, v := range g.adj_list{
        fmt.Printf("[%d] -> ", k)
        for current := v.Front(); current != nil; current = current.Next(){
            // current.Value 為 interface{}
            // 利用 .(*Vertex) 斷言,告訴 Go 這是一個 *Vertex
            // 再對 *Vertex 取值
            fmt.Printf("%d -> ", current.Value.(*Vertex).Val)
        }
        fmt.Println("nil")
    }
}
其中,可能令人比較陌生的是「斷言」這種用法,這個是用來給 interface{} 型態使用,如果原先以 int 當 interface{} 來存,在取用時,必需先用 .(int) 來「轉」換型態。並不是每個情況取用都一定要轉,新手魔法使們多試幾次有噴錯就知道什麼時候要「斷言」了
實際調用:
本教學採用新的套件模式 (go version >= 1.11)
預設 module 名稱是 practice ,所以要 import "practice/graph"
./lesson25.go
package main
import(
    "practice/graph"
)
func main(){
    // 範例中有六個節點
    g  := graph.NewGraph(6)
    v0 := graph.NewVertex(0)
    v1 := graph.NewVertex(1)
    v2 := graph.NewVertex(2)
    v3 := graph.NewVertex(3)
    v4 := graph.NewVertex(4)
    v5 := graph.NewVertex(5)
    g.AddEdge(v0, v1)
    g.AddEdge(v0, v2)
    g.AddEdge(v2, v3)
    g.AddEdge(v2, v4)
    g.AddEdge(v3, v0)
    g.AddEdge(v3, v5)
    g.AddEdge(v4, v1)
    g.AddEdge(v4, v5)
    g.EasyTraversal()
}
執行結果:
[0] -> 1 -> 2 -> nil
[1] -> nil
[2] -> 3 -> 4 -> nil
[3] -> 0 -> 5 -> nil
[4] -> 1 -> 5 -> nil
[5] -> nil
剛剛介紹的圖是用鍵頭連結代表是有方向性的有向圖。如果單純以直線連接那麼就是無向圖,無向圖代表可以從直線的兩端是可以互通
↓ 這是一個無向圖

修改 AddEdge() 使兩個方向都能互通
./graph/graph.go
func (g *Graph) AddEdge(start, end *Vertex){
    g.adj_list[start.Val].PushBack(end)
    g.adj_list[end.Val].PushBack(start)
}
重新執行 ./lesson25.go
執行結果:
[0] -> 1 -> 2 -> 3 -> nil
[1] -> 0 -> 4 -> nil
[2] -> 0 -> 3 -> 4 -> nil
[3] -> 2 -> 0 -> 5 -> nil
[4] -> 2 -> 1 -> 5 -> nil
[5] -> 3 -> 4 -> nil
如此一來,點和點間就能更簡單的往來,那為什麼要講無向圖呢?因為無向圖比較好走訪,為了明天的教學做準備

小狼你是在害羞什麼,小櫻可是你的敵人吔
本集圖片來自: