鏈表
Go語言的鏈表實現在其標準庫的container/list代碼包中。
這個包包含了2個程序實體:
- List : 實現了一個雙向鏈表
- Element : 代表了鏈表中元素的結構
操作鏈表
移動鏈表裏的元素:
func (l *List) MoveBefore(e, mark *Element) // 把元素e移動到mark元素的前面
func (l *List) MoveAfter(e, mark *Element) // 把元素e移動到mark元素的後面
func (l *List) MoveToFront(e *Element) // 把元素e移動到鏈表的最前端
func (l *List) MoveToBack(e *Element) // 把元素e移動到鏈表的最後端
上面的方法都是調整鏈表l裏元素的位置,e和mark原本都是鏈表裏的元素,執行方法後,只是調整元素e在鏈表中的位置。所以操作前後鏈表裏包含的元素並不會有差別,只是e元素的位置可能變化了。
添加元素
鏈表裏的元素都是*Element類型。List包中那些用於插入新元素的方法都只接收interface{}類型的值。這個方法內部都會用Element包裝接收到的新元素:
func (l *List) InsertBefore(v interface{}, mark *Element) *Element // 在mark元素之前插入行元素
func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在mark元素之後插入行元素
func (l *List) PushFront(v interface{} *Element) *Element // 在鏈表的最前端添加新元素
func (l *List) PushBack(v interface{} *Element) *Element // 在鏈表的最後端添加新元素
上面的方法都會返回一個指針*Element。也就是插入元素的*Element類型。
獲取元素
通過鏈表,可以直接取到鏈表頭尾的元素,這是一個雙向鏈表。然後有了鏈表中的某個元素之後,就可以拿到該元素前一個或後一個元素了:
func (l *List) Front() *Element // 獲取到鏈表中最前端的元素
func (l *List) Back() *Element // 獲取到鏈表中最後端的元素
func (e *Element) Next() *Element // 獲取當前元素的下一個元素
func (e *Element) Prev() *Element // 獲取當前元素的前一個元素
鏈表的機制
下面是官方文檔裏的List類型的描述,隱藏了私有字段:
type List struct {
// contains filtered or unexported fields
}
List這個結構體類型有兩個字段,一個是Element類型的字段root,代碼鏈表的根元素;另一個是int類型的字段len,代表鏈表的長度。都是包級私有的,我們無法查看和修改它們。
下面是Element類型的描述,同樣的隱藏了私有字段:
type Element struct {
// The value stored with this element.
Value interface{}
// contains filtered or unexported fields
}
Element類型裏分別有一個用於存儲前一個元素和後一個元素以及所屬鏈表的指針值。另外還有一個公開字段Value,就是元素的值。
延遲初始化機制
所謂延遲初始化,你可以理解爲把初始化操作延後,僅在實際需要的時候才進行。延遲初始化的優點在於“延後”,它可以分散初始化操作帶來的計算量和存儲空間消耗。
然而,延遲初始化的缺點恰恰也在於“延後”。如果在調用鏈表的每個方法的時候,都需要去判斷鏈表是否已經被初始化的話,那麼也是一個計算量上的浪費。
在這裏的鏈表的實現中,一些方法是無需對是否初始化做判斷的。比如:
Front和Back方法,一旦發現鏈表的長度爲0,就可以直接返回nil。
刪除、移動、刪除鏈表元素時,判斷一下傳入元素中的所屬鏈表的指針,是否與當前鏈表的指針相同。相等,就說明這個鏈表已經被初始化了,否則說明元素在不要操作的鏈表中,那麼就直接返回。
上面的操作,應該都是要鏈表是已經完成初始化的,但是未初始化過的鏈表,通過上面的機制,也能正確返回。這樣初始化的操作就可以只在必要的時候才進行,比如:
PushFront、PushBack、PushBackList、PushFrontList,這些方法,會先判斷鏈表的動態。如果沒有初始化,就進行初始化。
所以,List利用了自身,以及Element在結構上的特點,平衡了延遲初始化的優缺點。
循環鏈表
在Go標準庫的container/ring包中的Ring類型實現的是一個循環鏈表。
type Ring struct {
Value interface{} // for use by client; untouched by this library
// contains filtered or unexported fields
}
其實List在內部就是一個循環鏈表。它的根元素永遠不會持有任何實際的元素值,而該元素的存在,就是爲了連接這個循環鏈表的首尾兩端。
所以,List的零值是一個只包含了根元素,但不包含任何實際元素值的空鏈表。
說List在內部就是一個循環鏈表,是它設計的邏輯,這個在最後我去源碼裏看了一下。這裏並不是指List是通過這裏的container/ring包實現的。而是List本身其實也是一個循環鏈表的結構,Ring是Go提供的一個實現循環鏈表的標準庫,Ring本身當然也是一個循環鏈表的結構。
Ring和List在本質上都是循環鏈表,主要有以下的幾點不同:
Ring類型的數據結構僅由它自身即可代表,而List類型則需要由它以及Element類型聯合表示。這是表示方式上的不同,也是結構複雜度上的不同。
Ring類型的值,只代表了其所屬的循環鏈表中的一個元素,而List類型的值則代表了一個完整的鏈表。這是表示維度上的不同。
在創建並初始化一個Ring值的時候,要指定它包含的元素的數量,但是List不能也不需要指定數量。這是兩個代碼包中的New函數在功能上的不同,也是兩個類型在初始化值方面的第一個不同。
通過var r ring.Ring語句聲明的r將會是一個長度爲1的循環鏈表,而List類型的零值則是一個長度爲0的鏈表。List中的根元素不會持有實際元素值,因此計算長度時不會包含它。這是兩個類型在初始化值方面的第二個不同。
Ring值的Len方法的算法複雜度是O(N)的,而List值的Len方法的算法複雜度則是 O(1)的。這是兩者在性能方面最顯而易見的差別。
關於上的len方法,因爲List的結構體裏直接就記了表示長度的私有字段len。而Ring不像List那樣有一個表示整個鏈表的結構體。兩個包裏的len方法的源碼如下:
// src/container/ring/ring.go
func (r *Ring) Len() int {
n := 0
if r != nil {
n = 1
for p := r.Next(); p != r; p = p.next {
n++
}
}
return n
}
// src/container/list/list.go
func (l *List) Len() int { return l.len }
總結
上面先講了鏈表,並且展開了鏈表的一些使用技巧和實現特點。由於鏈表本身內部就是一個循環鏈表。所以又和container/ring包中的循環鏈表做了一番比較。
另外,container一共有3個子包,上面講到了2個,還有一個是container/heap,就是堆。
List的循環結構
關於List內部就是一個循環鏈表的問題,自己又去源碼裏探究了一番。
下面是Init方法,把root元素的下一個元素和前一個元素都指向自己,形成了一個環。並且把長度字段len設置成0:
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
雖然List本質是個環,但是使用的時候,不是環而是有頭和尾的一條鏈。在獲取下一個元素的時候,如果到了最後端,那麼next的下一個元素就是root元素。這時不返回root,而是返回nil。這就是根元素不持有任何元素,只是連接鏈表的首尾兩端:
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}