這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
今天看tcp/ip協議棧的代碼時看到一個雙向鏈表,鏈表嗎?聽過它的頂頂大名,知道它是由節點構成的,每個節點還有個指標指向下一個節點,但是從來沒自己實現過一個,沒有實踐就不能深刻理解,遂有此文。
以下所有觀點都是個人愚見,有不同建議或補充的的歡迎emial我aboutme
何為鏈表?
鏈表(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。由於不必須按順序儲存,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是尋找一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
簡單的說鏈表是一個具有邏輯順序的線性表,每一個節點裡存到下一個節點的指標。
圖示
單鏈表
雙向鏈表
鏈表有啥用?
因為鏈表插入很快,而且具有動態性,想添加幾個元素就添加幾個(記憶體空間足夠),不像數組一樣那麼死板,正因為鏈表的靈活性,所有鏈表的用處是大大的有啊。
鏈表最適合用於頻繁更新變化
的資料,比如一個需要非同步執行並且不可丟失的命令序列、一個需要進行即時載入與卸載的驅動,無序且數量未知,這個時候就需要鏈表結構來協助完成資料的管理。如果不需要過度關注資料的順序,還可以用鏈表方便快捷地在任意一個地方插入或刪除一個元素,並且不會影響到其它的元素。
又或者我在今天看tcp/ip源碼中,鏈表用來構造隊列,作為資料區段的隊列。我想鏈表用於隊列應該是最多的。如果你看過linux核心源碼,應該會發現linux核心中多處使用鏈表這種結構。
go標準庫的雙向鏈表
golang的標準庫中實現了一個雙向鏈表,該鏈表可以儲存任何資料,先看看使用標準庫鏈表的例子:
package list_testimport ("container/list""fmt""testing")func TestList(t *testing.T) {// Create a new list and put some numbers in it.l := list.New()e4 := l.PushBack(4)e1 := l.PushFront(1)l.InsertBefore(3, e4)l.InsertAfter(2, e1)// Iterate through list and print its contents.for e := l.Front(); e != nil; e = e.Next() {fmt.Println(e.Value)}}// output// 1// 2// 3// 4
該鏈表實現了鏈表所有的功能,鏈表的增刪查改。
實現該鏈表的資料結構
// List represents a doubly linked list.// The zero value for List is an empty list ready to use.type List struct {root Element // sentinel list element, only &root, root.prev, and root.next are usedlen int // current list length excluding (this) sentinel element}// Element is an element of a linked list.type Element struct {// Next and previous pointers in the doubly-linked list of elements.// To simplify the implementation, internally a list l is implemented// as a ring, such that &l.root is both the next element of the last// list element (l.Back()) and the previous element of the first list// element (l.Front()).next, prev *Element// The list to which this element belongs.list *List// The value stored with this element.Value interface{}}
可以看到Element
結構體看到了鏈表的結構,next
,prev
分別指向下一個和前一個元素的指標。Value
就是鏈表中的資料區段,可以理解為中的object。
介入式鏈表(intrusive list)
前面的鏈表都是普通鏈表,記得<<c語言程式設計>>
上講的鏈表也是一樣,就是鏈表的節點指標和資料區段是放在同一個struct,每實現一個不同的struct就得重新實現一遍鏈表的功能,這對於“懶惰”的程式員來說是不可忍受的,所以就出來了介入式鏈表,將資料區段和鏈表的功能區別開來。最經典的例子莫過於linux核心的list_head,詳情請看連結klist or Linked List in Linux Kernel,linux中是用c實現的,我想用go實現一個介入式鏈表。
實現代碼
package listtype Intrusive interface {Next() IntrusivePrev() IntrusiveAddNext(Intrusive)AddPrev(Intrusive)}// List provides the implementation of intrusive linked liststype List struct {prev Intrusivenext Intrusive}func (l *List) Next() Intrusive {return l.next}func (l *List) Prev() Intrusive {return l.prev}func (l *List) AddNext(i Intrusive) {l.next = i}func (l *List) AddPrev(i Intrusive) {l.prev = i}func (l *List) Reset() {l.prev = nill.next = nil}func (l *List) Empty() bool {return l.prev == nil}// Front returns the first element of list l or nil.func (l *List) Front() Intrusive {return l.prev}// Back returns the last element of list l or nil.func (l *List) Back() Intrusive {return l.next}// PushFront inserts the element e at the front of list l.func (l *List) PushFront(e Intrusive) {e.AddPrev(nil)e.AddNext(l.prev)if l.prev != nil {l.prev.AddPrev(e)} else {l.next = e}l.prev = e}// PushBack inserts the element e at the back of list l.func (l *List) PushBack(e Intrusive) {e.AddNext(nil)e.AddPrev(l.next)if l.next != nil {l.next.AddNext(e)} else {l.prev = e}l.next = e}// InsertAfter inserts e after b.func (l *List) InsertAfter(e, b Intrusive) {a := b.Next()e.AddNext(a)e.AddPrev(b)b.AddNext(e)if a != nil {a.AddPrev(e)} else {l.next = e}}// InsertBefore inserts e before a.func (l *List) InsertBefore(e, a Intrusive) {b := a.Prev()e.AddNext(a)e.AddPrev(b)a.AddPrev(e)if b != nil {b.AddNext(e)} else {l.prev = e}}// Remove removes e from l.func (l *List) Remove(e Intrusive) {prev := e.Prev()next := e.Next()if prev != nil {prev.AddNext(next)} else {l.prev = next}if next != nil {next.AddPrev(prev)} else {l.next = prev}}
我們這裡用List
表示實現了Intrusive
介面,也實現了鏈表的準系統,所以任何實現了Intrusive
介面的對象都是可以作為鏈表的節點,利用這個介入式鏈表就很簡單了,只要在現有的struct嵌入List
這個結構體即可,在舉個例子:
package listimport ("fmt""testing")func TestIntrusiveList(t *testing.T) {type E struct {Listdata int}// Create a new list and put some numbers in it.l := List{}e4 := &E{data: 4}e1 := &E{data: 1}l.PushBack(e4)l.PushFront(e1)l.InsertBefore(&E{data: 3}, e4)l.InsertAfter(&E{data: 2}, e1)for e := l.Front(); e != nil; e = e.Next() {fmt.Printf("e: %+v\n", e)fmt.Printf("data: %d\n", e.(*E).data)}}// output// e: &{List:{prev:<nil> next:0xc4200789c0} data:1}// data: 1// e: &{List:{prev:0xc420078960 next:0xc420078990} data:2}// data: 2// e: &{List:{prev:0xc4200789c0 next:0xc420078930} data:3}// data: 3// e: &{List:{prev:0xc420078990 next:<nil>} data:4}// data: 4
在E
裡嵌入List
即可作為鏈表的節點,是不是很方便,其實當我寫完介入式鏈表的栗子後,發現其實標準庫的鏈表更方便,哈哈。。因為golang有interface{}
。
參考
https://blog.goquxiao.com/posts/2013/07/06/intrusive-list/
http://blog.nlogn.cn/linked-list-in-linux-kernel/