數據結構和算法(Golang實現)(13)常見數據結構-可變長數組

可變長數組

由於數組大小是固定的,當數據元素特別多時,固定的數組沒法儲存這麼多的值,因此可變長數組出現了,這也是一種數據結構。在Golang語言中,可變長數組被內置在語言裏面:切片slice算法

slice是對底層數組的抽象和控制。它是一個結構體:segmentfault

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  1. 指向底層數組的指針。(Golang語言是沒有操做原始內存的指針的,因此unsafe包提供相關的對內存指針的操做,通常狀況下非專業人員勿用)
  2. 切片的真正長度,也就是實際元素佔用的大小。
  3. 切片的容量,底層固定數組的長度。

每次能夠初始化一個固定容量的切片,切片內部維護一個固定大小的數組。當append新元素時,固定大小的數組不夠時會自動擴容,如:數組

package main

import "fmt"

func main() {
    // 建立一個容量爲2的切片
    array := make([]int, 0, 2)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)

    // 雖然 append 可是沒有賦予原來的變量 array
    _ = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    _ = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    _ = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)

    fmt.Println("-------")

    // 賦予回原來的變量
    array = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    array = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    array = append(array, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    array = append(array, 1, 1, 1, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
    array = append(array, 1, 1, 1, 1, 1, 1, 1, 1, 1)
    fmt.Println("cap", cap(array), "len", len(array), "array:", array)
}

輸出:安全

cap 2 len 0 array: []
cap 2 len 0 array: []
cap 2 len 0 array: []
cap 2 len 0 array: []
-------
cap 2 len 1 array: [1]
cap 2 len 2 array: [1 1]
cap 4 len 3 array: [1 1 1]
cap 8 len 7 array: [1 1 1 1 1 1 1]
cap 16 len 16 array: [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

咱們能夠看到Golang的切片沒法原地append,每次添加元素時返回新的引用地址,必須把該引用從新賦予以前的切片變量。而且,當容量不夠時,會自動倍數遞增擴容。事實上,Golang在切片長度大於1024後,會以接近於1.25倍進行容量擴容。數據結構

具體可參考標準庫runtime下的slice.go文件。併發

1、實現可變長數組

咱們來實現一個簡單的,存放整數的,可變長的數組版本。app

由於Golang的限制,不容許使用[n]int來建立一個固定大小爲n的整數數組,只容許使用常量來建立大小。數據結構和算法

因此咱們這裏會使用切片的部分功能來代替數組,雖然切片自己是可變長數組,可是咱們不會用到它的append功能,只把它當數組用。函數

import (
    "sync"
)

// 可變長數組
type Array struct {
    array []int      // 固定大小的數組,用滿容量和滿大小的切片來代替
    len   int        // 真正長度
    cap   int        // 容量
    lock  sync.Mutex // 爲了併發安全使用的鎖
}

1.1. 初始化數組

建立一個len個元素,容量爲cap的可變長數組:指針

// 新建一個可變長數組
func Make(len, cap int) *Array {
    s := new(Array)
    if len > cap {
        panic("len large than cap")
    }

    // 把切片當數組用
    array := make([]int, cap, cap)

    // 元數據
    s.array = array
    s.cap = cap
    s.len = 0
    return s
}

主要利用滿容量和滿大小的切片來充當固定數組,結構體Array裏面的字段lencap來控制值的存取。不容許設置len > cap的可變長數組。

時間複雜度爲:O(1),由於分配內存空間和設置幾個值是常數時間。

1.2. 添加元素

// 增長一個元素
func (a *Array) Append(element int) {
    // 併發鎖
    a.lock.Lock()
    defer a.lock.Unlock()

    // 大小等於容量,表示沒多餘位置了
    if a.len == a.cap {
        // 沒容量,數組要擴容,擴容到兩倍
        newCap := 2 * a.len

        // 若是以前的容量爲0,那麼新容量爲1
        if a.cap == 0 {
            newCap = 1
        }

        newArray := make([]int, newCap, newCap)

        // 把老數組的數據移動到新數組
        for k, v := range a.array {
            newArray[k] = v
        }

        // 替換數組
        a.array = newArray
        a.cap = newCap

    }

    // 把元素放在數組裏
    a.array[a.len] = element
    // 真實長度+1
    a.len = a.len + 1

}

首先添加一個元素到可變長數組裏,會加鎖,這樣會保證併發安全。而後將值放在數組裏:a.array[a.len] = element,而後len + 1,表示真實大小又多了一個。

當真實大小len = cap時,代表位置都用完了,沒有多餘的空間放新值,那麼會建立一個固定大小2*len的新數組來替換老數組:a.array = newArray,固然容量也會變大:a.cap = newCap。若是一開始設置的容量cap = 0,那麼新的容量會是從 1 開始。

添加元素中,耗時主要在老數組中的數據移動到新數組,時間複雜度爲:O(n)。固然,若是容量夠的狀況下,時間複雜度會變爲:O(1)

如何添加多個元素:

// 增長多個元素
func (a *Array) AppendMany(element ...int) {
    for _, v := range element {
        a.Append(v)
    }
}

只是簡單遍歷一下,調用Append函數。其中...intGolang的語言特徵,表示多個函數變量。

1.3. 獲取指定下標元素

// 獲取某個下標的元素
func (a *Array) Get(index int) int {
    // 越界了
    if a.len == 0 || index >= a.len {
        panic("index over len")
    }
    return a.array[index]
}

當可變長數組的真實大小爲0,或者下標index超出了真實長度len,將會panic越界。

由於只獲取下標的值,因此時間複雜度爲O(1)

1.4. 獲取真實長度和容量

// 返回真實長度
func (a *Array) Len() int {
    return a.len
}

// 返回容量
func (a *Array) Cap() int {
    return a.cap
}

時間複雜度爲O(1)

1.5. 示例

如今咱們來運行完整的可變長數組的例子:

package main

import (
    "fmt"
    "sync"
)

// 可變長數組
type Array struct {
    array []int      // 固定大小的數組,用滿容量和滿大小的切片來代替
    len   int        // 真正長度
    cap   int        // 容量
    lock  sync.Mutex // 爲了併發安全使用的鎖
}

// 新建一個可變長數組
func Make(len, cap int) *Array {
    s := new(Array)
    if len > cap {
        panic("len large than cap")
    }

    // 把切片當數組用
    array := make([]int, cap, cap)

    // 元數據
    s.array = array
    s.cap = cap
    s.len = 0
    return s
}

// 增長一個元素
func (a *Array) Append(element int) {
    // 併發鎖
    a.lock.Lock()
    defer a.lock.Unlock()

    // 大小等於容量,表示沒多餘位置了
    if a.len == a.cap {
        // 沒容量,數組要擴容,擴容到兩倍
        newCap := 2 * a.len

        // 若是以前的容量爲0,那麼新容量爲1
        if a.cap == 0 {
            newCap = 1
        }

        newArray := make([]int, newCap, newCap)

        // 把老數組的數據移動到新數組
        for k, v := range a.array {
            newArray[k] = v
        }

        // 替換數組
        a.array = newArray
        a.cap = newCap

    }

    // 把元素放在數組裏
    a.array[a.len] = element
    // 真實長度+1
    a.len = a.len + 1

}

// 增長多個元素
func (a *Array) AppendMany(element ...int) {
    for _, v := range element {
        a.Append(v)
    }

}

// 獲取某個下標的元素
func (a *Array) Get(index int) int {
    // 越界了
    if a.len == 0 || index >= a.len {
        panic("index over len")
    }
    return a.array[index]
}

// 返回真實長度
func (a *Array) Len() int {
    return a.len
}

// 返回容量
func (a *Array) Cap() int {
    return a.cap
}

// 輔助打印
func Print(array *Array) (result string) {
    result = "["
    for i := 0; i < array.Len(); i++ {
        // 第一個元素
        if i == 0 {
            result = fmt.Sprintf("%s%d", result, array.Get(i))
            continue
        }

        result = fmt.Sprintf("%s %d", result, array.Get(i))
    }
    result = result + "]"
    return
}

func main() {
    // 建立一個容量爲3的動態數組
    a := Make(0, 3)
    fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))

    // 增長一個元素
    a.Append(10)
    fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))

    // 增長一個元素
    a.Append(9)
    fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))

    // 增長多個元素
    a.AppendMany(8, 7)
    fmt.Println("cap", a.Cap(), "len", a.Len(), "array:", Print(a))
}

將打印出:

cap 3 len 0 array: []
cap 3 len 1 array: [10]
cap 3 len 2 array: [10 9]
cap 6 len 4 array: [10 9 8 7]

能夠看到,容量會自動翻倍。

2、總結

可變長數組在實際開發上,常常會使用到,其在固定大小數組的基礎上,會自動進行容量擴展。

由於這一數據結構的使用頻率過高了,因此,Golang自動提供了這一數據類型:切片(可變長數組)。你們通常開發過程當中,直接使用這一類型便可。

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索