Go 語言實現動態數組

數組設計之初是在形式上依賴內存分配而成的,因此必須在使用前預先請求空間。這使得數組有如下特性:git

  1. 請求空間之後大小固定,不能再改變(數據溢出問題);
  2. 在內存中有空間連續性的表現,中間不會存在其餘程序須要調用的數據,爲此數組的專用內存空間;
  3. 在舊式編程語言中(若有中階語言之稱的C),程序不會對數組的操做作下界判斷,也就有潛在的越界操做的風險(好比會把數據寫在運行中程序須要調用的核心部分的內存上)。

以上引用自維基百科github

傳統數組的侷限性致使了動態數組的誕生。然而動態數組也不是使用動態的內存,依舊是一塊連續的內存。那它是如何實現數組大小不固定的呢?緣由是當超過數組容量時,程序將自動執行擴容操做:編程

  1. 從新開闢一塊大小爲當前數組容量兩倍的內存
  2. 把原數組的數據拷貝到此內存空間
  3. 釋放原數組的內存

本文將從概念上實現動態數組的擴容和縮容特性,並實現數組增刪查改操做的方法。數組

查看 Github 代碼app

數組結構

咱們自定義的數組結構 Array 基於 Go 的 slice 實現,維護 size 字段可讓咱們方便的獲取數組元素的個數,當咱們須要獲取元素總數時,就不須要循環 data 去計算。編程語言

type Array struct {
    data []interface{}  // 泛型數組
    size int            // 元素數量
}

數組接口

使用接口聲明咱們須要實現的方法,Go 語言沒有繼承的關鍵字,實現接口定義的全部方法就會自動繼承該接口。函數

type ArrayInterface interface {
    // 添加
    Add(int, interface{})    // 插入元素
    AddLast(interface{})
    AddFirst(interface{})
    // 刪除
    Remove(int) interface{}
    RemoveFirst() interface{}
    RemoveLast() interface{}
    // 查找
    Find(interface{}) int // 查找元素返回第一個索引
    FindAll(interface{}) []int // 查找元素返回全部索引
    Contains(interface{}) bool // 查找是否存在元素
    Get(int) interface{}
    // 修改
    Set(int, interface{})
    // 基本方法
    GetCapacity() int // 得到數組容量
    GetSize() int  // 得到元素個數
    IsEmpty() bool // 查看數組是否爲空
}

數組實現

擴/縮容規則

數組 resize 指當數組元素超過數組容量,或者元素小於數組容量時,須要完成的擴容和縮容規則:測試

  • 超過數組容量,按照當前容量的 2 倍擴容。
  • 數組元素個數爲當前容量 1/4 時,縮容爲當前容量的一半。

爲何縮容不是 1/2?ui

若是在 1/2 時縮容,會致使在擴容的臨界點添加、刪除一個元素都是 O(n) 複雜度的狀況(臨界點添加一個元素,致使擴容爲 2 倍,此時刪除剛添加的元素,又會縮容爲 1/2)。設計

數組的方法實現

構造函數

Go 沒有提供構造函數,咱們能夠聲明一個公共的函數代替

// 得到自定義數組,參數爲數組的初始長度
func GetArray(capacity int) *Array {
    arr := &Array{}
    arr.data = make([]interface{}, capacity)
    arr.size = 0
    return arr
}

基本方法

// 得到數組容量
func (a *Array) GetCapacity() int {
    return len(a.data)
}
// 得到數組元素個數
func (a *Array) GetSize() int {
    return a.size
}
// 判斷數組是否爲空
func (a *Array) IsEmpty() bool {
    return a.size == 0
}

數組容量調整

容量調整的邏輯爲,聲明一個新的數組,將原數組的元素賦值給新數組。

// newCapacity 新數組容量
// 邏輯:聲明新的數組,將原數組的值 copy 到新數組中
func (a *Array) resize(newCapacity int) {
    newArr := make([]interface{}, newCapacity)
    for i := 0; i < a.size; i++ {
        newArr[i] = a.data[i]
    }
    a.data = newArr
}

查找元素

查找元素指輸入元素返回元素的索引

// 得到元素的首個索引,不存在則返回 -1
func (a *Array) Find(element interface{}) int {
    for i:= 0; i < a.size; i++ {
        if element == a.data[i] {
            return i
        }
    }
    return -1
}
// 得到元素的全部索引,返回索引組成的切片
func (a *Array) FindAll(element interface{}) (indexes []int) {
    for i := 0; i < a.size; i++ {
        if element == a.data[i] {
            indexes = append(indexes, i)
        }
    }
    return
}
// 查看數組是否存在元素,返回 bool
func (a *Array) Contains(element interface{}) bool {
    if a.Find(element) == -1 {
        return false
    }
    return true
}
// 得到索引對應元素,須要判斷索引有效範圍
func (a *Array) Get(index int) interface{} {
    if index < 0 || index > a.size - 1 {
        panic("Get failed, index is illegal.")
    }
    return a.data[index]
}

修改元素

修改索引對應元素值
func (a *Array) Set(index int, element interface{}) {
    if index < 0 || index > a.size - 1 {
        panic("Set failed, index is illegal.")
    }
    a.data[index] = element
}

添加元素

添加元素須要考慮擴容問題,同時 AddLastAddFirst 都是基於 Add 實現的,這很是的方便。

func (a *Array) Add(index int, element interface{}) {
    if index < 0 || index > a.GetCapacity() {
        panic("Add failed, require index >= 0 and index <= capacity")
    }
    // 數組已滿則擴容
    if a.size == len(a.data) {
        a.resize(2 * a.size)
    }
    // 將插入的索引位置以後的元素後移,騰出插入位置
    for i := a.size - 1; i >= index; i-- {
        a.data[i + 1] = a.data[i]
    }
    a.data[index] = element
    // 維護數組元素的數量
    a.size++
}

func (a *Array) AddLast(element interface{}) {
    a.Add(a.size, element)
}

func (a *Array) AddFirst(element interface{}) {
    a.Add(0, element)
}

刪除元素

刪除元素須要考慮縮容問題

func (a *Array) Remove(index int) interface{} {
    if index < 0 || index >= a.size {
        panic("Remove failed, index is illegal.")
    }

    removeEle := a.data[index]
    // 從 index 以後的元素,都向前移動一個位置
    for i := index + 1; i < a.size; i++ {
        a.data[i-1] = a.data[i]
    }
    a.size--
    // 清理最後一個元素
    a.data[a.size] = nil

    // 考慮邊界狀況,不能 resize 爲0
    if a.size == len(a.data)/4 && len(a.data)/2 != 0 {
        a.resize(len(a.data) / 2)
    }
    return removeEle
}

func (a *Array) RemoveFirst() interface{} {
    return a.Remove(0)
}

func (a *Array) RemoveLast() interface{} {
    return a.Remove(a.size - 1)
}

重寫 String 方法

重寫數組打印時的展現形式,只須要重寫 String 方法

func (a *Array) String() string {
    var buffer bytes.Buffer
    buffer.WriteString(fmt.Sprintf("Array: size = %d, capacity = %d\n", a.size, a.GetCapacity()))
    buffer.WriteString("[")
    for i := 0; i < a.size; i++ {
        buffer.WriteString(fmt.Sprint(a.data[i]))
        if i != a.size - 1 {
            buffer.WriteString(",")
        }
    }
    buffer.WriteString("]")
    return buffer.String()
}

最終測試

func main() {
    arr := GetArray(10)
    for i := 0; i < 10; i++ {
        arr.AddLast(i)
    }
    fmt.Println(arr)

    arr.Add(1, 100)
    fmt.Println(arr)

    arr.AddFirst(-1)
    fmt.Println(arr)
}

輸出結果:

Array: size = 10, capacity = 10
[0,1,2,3,4,5,6,7,8,9]
Array: size = 11, capacity = 20
[0,100,2,2,3,4,5,6,7,8,9]
Array: size = 12, capacity = 20
[-1,100,100,2,2,3,4,5,6,7,8,9]

時間複雜度分析

添加操做

  • Add(int, interface{}) O(n)
  • AddLast(interface{}) O(1)
  • AddFirst(interface{}) O(n)

AddLast 涉及到擴容的操做,容量爲 n 的數組添加 n + 1 個元素,會操做 2n + 1 次,由於第 n + 1 次操做致使擴容,原數組須要copy n 次,其平均操做次數爲 2 次,因此均攤複雜度是O(1)。

刪除操做

  • Remove(int) interface{} O(n)
  • RemoveFirst() interface{} O(n)
  • RemoveLast() interface{} O(1)

RemoveFirst 會將第一個元素以後的全部元素前移一個位置,Remove 也多是移除第一個元素,它們都是 n 的複雜度,而 RemoveLast 只須要移除最後一個元素。

查找操做

  • Find(interface{}) int O(n)
  • FindAll(interface{}) []int O(n)
  • Contains(interface{}) bool O(n)
  • Get(int) interface{} O(1)

Find 類的操做都須要遍歷數組,爲 n 的複雜度。Get 能夠直接經過鍵得到值,這也是數組的優點所在。

修改操做

  • Set(int, interface{}) O(1)

總結

能夠發現,數組對已知索引和數組尾部的操做都是 O(1) 的複雜度,咱們能夠將這些理解爲數組的優點,當咱們用數組實現一些功能就能夠充分利用這些優點。

例如,咱們用數組模擬一個棧的操做,若是用數組尾部模擬棧頂,入棧、出棧都是都是 O(1) 時間複雜度,這是很是高效的。可是將出入棧放到數組頭部,時間複雜度就變成 O(n) 了,差距是顯而易見。

相關文章
相關標籤/搜索