Go 的容器數據結構

前言

Java 內置了豐富的容器類,不一樣容器用於處理各類業務場景。 Go 雖然語言設計上和 Java 有不少類似的地方, 但原生並無支持太多容器類的數據結構,只有 map 和 slice。標準庫的 container package 對容器數據結構作了擴展,支持堆(Heap)、鏈表(LinkedList) 和循環鏈表(Circular List)3個容器。html

容器

熟悉 C++ 和 Java 對容器應該都有清晰的瞭解, 它是現代編程實踐中不可或缺的一部分,具備多種形式, 通常被描述爲具備操做容器內容的方法的對象。Go 提供的基本容器主要有6個:git

  • 內置容器:golang

    • map: 關聯容器
    • slice: 動態擴容的順序容器
  • channels:隊列
  • container標準庫(pkg/container):算法

    • list:鏈表容器
    • ring:循環鏈表容器
    • heap: 堆容器,提供 heap 的實現

slice 、map 和 channel 是 Go 最多見、也是內置的容器數據結構,其餘容器都在標準庫的 container 包下。在使用 container 三個容器的時候,沒必要再費心實現數據結構相關的算法。同時,由於container 提供的容器支持的入參類型都是 interface{}, 因此只要實現了容器的 interface, 就能夠處理任何類型的值。編程

container/list

鏈表容器 list 的代碼是一個雙向鏈表的實現。list 維護兩個結構體:Element 和 List:數組

// 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{}
}

// 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 used
    len  int     // current list length excluding (this) sentinel element
}

linked-list

當經過 list.New() 建立一個 list 時,會初始化一個 Element 做爲 Root Pointer,它要麼指向列表的初始元素,要麼爲 nil。每個 Element 除了數據字段Value外,還有 prevnext 分別指向 直接前驅 和 直接後繼, 來容許用戶在 list 中先後移動元素。緩存

list 容器支持的方法以下:數據結構

type Element
    func (e *Element) Next() *Element
    func (e *Element) Prev() *Element
type List
    func New() *List
    func (l *List) Back() *Element   // 最後一個元素
    func (l *List) Front() *Element  // 第一個元素
    func (l *List) Init() *List  // 鏈表初始化
    func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在某個元素後插入
    func (l *List) InsertBefore(v interface{}, mark *Element) *Element  // 在某個元素前插入
    func (l *List) Len() int // 在鏈表長度
    func (l *List) MoveAfter(e, mark *Element)  // 把 e 元素移動到 mark 以後
    func (l *List) MoveBefore(e, mark *Element)  // 把 e 元素移動到 mark 以前
    func (l *List) MoveToBack(e *Element) // 把 e 元素移動到隊列最後
    func (l *List) MoveToFront(e *Element) // 把 e 元素移動到隊列最頭部
    func (l *List) PushBack(v interface{}) *Element  // 在隊列最後插入元素
    func (l *List) PushBackList(other *List)  // 在隊列最後插入接上新隊列
    func (l *List) PushFront(v interface{}) *Element  // 在隊列頭部插入元素
    func (l *List) PushFrontList(other *List) // 在隊列頭部插入接上新隊列
    func (l *List) Remove(e *Element) interface{} // 刪除某個元素

下面是 list 的一個簡單例子:app

package main

import (
    "container/list"
    "fmt"
)

func main() {
    // 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.Printf(".2d",e.Value)
    }

}

這段代碼的輸出是: 1234。在初始化 list 後,在尾結點插入4,在頭結點插入1;再在4前插入3,在1後插入2,因此結果是1234。ide

list 在插入、刪除數據的時間複雜度在 $O(1)$; 隨機查找效率較低,爲 $O(N)$ (slice 隨機查找的時間效率爲 $O(1)$

鏈表容器常見的應用場景是用於作 LRU 緩存

container/ring

循環鏈表容器 ring 是一個沒有頭節點和尾節點的鏈表,這裏能夠當作是一個簡化版的 list。ring 維護一個結構體 Ring:

// A Ring is an element of a circular list, or ring.
// Rings do not have a beginning or end; a pointer to any ring element
// serves as reference to the entire ring. Empty rings are represented
// as nil Ring pointers. The zero value for a Ring is a one-element
// ring with a nil Value.
//
type Ring struct {
    next, prev *Ring
    Value      interface{} // for use by client; untouched by this library
}

ring

但跟 list 不一樣的是 ring 的方法是不同的:

type Ring
    func New(n int) *Ring  // 初始化環
    func (r *Ring) Do(f func(interface{}))  // 循環環進行操做
    func (r *Ring) Len() int // 環長度
    func (r *Ring) Link(s *Ring) *Ring // 鏈接兩個環
    func (r *Ring) Move(n int) *Ring // 指針從當前元素開始向後移動或者向前(n 能夠爲負數)
    func (r *Ring) Next() *Ring // 當前元素的下個元素
    func (r *Ring) Prev() *Ring // 當前元素的上個元素
    func (r *Ring) Unlink(n int) *Ring // 從當前元素開始,刪除 n 個元素

下面是 ring 的一個簡單例子:

package main

import (
    "container/ring"
    "fmt"
)

func main() {

    // Create a new ring of size 5
    r := ring.New(5)

    // Get the length of the ring
    n := r.Len()

    // Initialize the ring with some integer values
    for i := 0; i < n; i++ {
        r.Value = i
        r = r.Next()
    }

    // Iterate through the ring and print its contents
    r.Do(func(p interface{}) {
        fmt.Printf("%d", p.(int))
    })

}

這段代碼的輸出是01234。 在初始化 ring 後, 對每一個元素的 Value 賦值, 由於 ring 提供 Do 方法,因此遍歷到當前元素的是時候,執行 Print 函數打印結果。

從 ring 的實現上能夠知道相關操做的時間複雜度在$O(N)$。由於 ring 節點沒有頭尾區分和 FIFO 的特性,因此一個能用到的應用場景是環形緩衝區:在不消費資源的狀況下提供對緩衝區的互斥訪問 。

container/heap

堆(Heap)就是用數組實現的徹底二叉樹。根據堆的特性能夠分爲兩種:最大堆和最小堆,二者的區別在於節點的排序方式上:

  1. 在最大堆中,父節點的值比每個子節點的值都要大,堆最大元素在 root 節點
  2. 在最小堆中,父節點的值比每個子節點的值都要小, 堆最小元素在 root 節點

Go 的堆容器 heap 在實現上是一個最小堆,heap 維護一個 Interface 接口:

// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

除了 出堆方法Push()和 入堆方法push(), Interface 內聯了 sort.Interface, 它實現了三個方法:

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)

min-heap

只要實現了 Interface 方法的數據類型, 就知足構建最小堆條件:

!h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()

經過 heap.Init() 函數構建一個最小堆(按先序遍歷排序的 slice),內部實現的 up()down() 分別來對 堆來進行 上調整 和 下調整。

  • Push():當往堆中插入一個元素的時候,這個元素插入到最右子樹的最後一個節點中,而後調用up() 向上保證最小堆。
  • Pop():當要從堆中推出一個元素的時候,先把這個元素和右子樹最後一個節點交換,而後彈出最後一個節點,而後對 root 調用 down(),向下保證最小堆。

heap 容器支持的方法以下:

func Fix(h Interface, i int) // 在 i 位置更新數據,重建最小堆
func Init(h Interface) // 初始化,把 h 構建成最小堆 
func Pop(h Interface) interface{} // 出堆操做
func Push(h Interface, x interface{}) // 入堆操做
func Remove(h Interface, i int) interface{} // 移除第 i 個元素

下面是使用 heap 容器的一個例子, 構建一個優先級隊列 pq:

package main

import (
    "container/heap"
    "fmt"
)

type PriorityQueue []*Item

func (pq PriorityQueue) Len() int           { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority > pq[j].priority }

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil  // avoid memory leak
    item.index = -1 // for safety
    *pq = old[0 : n-1]
    return item
}

func (pq *PriorityQueue) update(item *Item, value string, priority int) {
    item.value = value
    item.priority = priority
    heap.Fix(pq, item.index)
}

func main() {
    items := map[string]int{
        "banana": 3,
        "apple":  2,
        "pear":   4,
    }

    pq := make(PriorityQueue, len(items))
    i := 0
    for value, priority := range items {
        pq[i] = &Item{
            value:    value,
            priority: priority,
            index:    i,
        }
        i++
    }
    heap.Init(&pq)

    // insert a new item
    item := &Item{
        value:    "orange",
        priority: 1,
    }
    heap.Push(&pq, item)
    pq.update(item, item.value, 5)

    fmt.Printf("\nheap length: %d\n", len(pq))
    for pq.Len() > 0 {
        i := heap.Pop(&pq).(*Item)
        fmt.Printf("%.2d:%s\t", i.priority, i.value)
    }
    fmt.Printf("\nheap length: %d\n", len(pq))
}

這段代碼的輸出是05:orange 04:pear 03:banana 02:apple。 首先是定義一個 PriorityQueue的結構體數組做爲隊列,有個 priority字段標識優先級,在 Less() 方法裏比較兩個元素的優先級,隊列 update()用於更新元素的優先級。而後每次在執行 heap.Pop(&pq).(*Item)操做,會把最小堆裏高priority元素出堆。

heap 在初始化的時候,時間複雜度在 $O(N)$;入堆、出堆、移動元素和重建最小堆的時間複雜度都是 $O(logN)$

堆容器在實際應用上是比較常見的, 在生產上常常用於實現優先級隊列 和 高性能定時器。

小結

本文主要梳理了 Go 的 容器數據結構,分析標準庫裏 container包實現的三個容器:list、ring 還有較複雜的 heap, 介紹它們的實現、特性和使用場景。雖然 slice 和 map 做爲在 Go 中是常常被使用到的容器,但若是在實際開發中發現這兩個數據結構並不知足咱們的需求,能夠在 pkg/container 下搜索是否有可用到的數據結構。

參考

  1. container pkg
  2. Go Containers Explained In Color
  3. container — 容器數據類型:heap、list 和 ring
  4. Go Containers
  5. 堆 heap
  6. 數據結果--容器(集合)
  7. 數據結構:堆
  8. 數據結構|雙向鏈表簡單實現和圖示
相關文章
相關標籤/搜索