數組設計之初是在形式上依賴內存分配而成的,因此必須在使用前預先請求空間。這使得數組有如下特性:git
- 請求空間之後大小固定,不能再改變(數據溢出問題);
- 在內存中有空間連續性的表現,中間不會存在其餘程序須要調用的數據,爲此數組的專用內存空間;
- 在舊式編程語言中(若有中階語言之稱的C),程序不會對數組的操做作下界判斷,也就有潛在的越界操做的風險(好比會把數據寫在運行中程序須要調用的核心部分的內存上)。
以上引用自維基百科。github
傳統數組的侷限性致使了動態數組的誕生。然而動態數組也不是使用動態的內存,依舊是一塊連續的內存。那它是如何實現數組大小不固定的呢?緣由是當超過數組容量時,程序將自動執行擴容操做:編程
本文將從概念上實現動態數組的擴容和縮容特性,並實現數組增刪查改操做的方法。數組
查看 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
指當數組元素超過數組容量,或者元素小於數組容量時,須要完成的擴容和縮容規則:測試
爲何縮容不是 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 }
添加元素須要考慮擴容問題,同時 AddLast
和 AddFirst
都是基於 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
方法
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]
AddLast
涉及到擴容的操做,容量爲 n 的數組添加 n + 1 個元素,會操做 2n + 1 次,由於第 n + 1 次操做致使擴容,原數組須要copy n 次,其平均操做次數爲 2 次,因此均攤複雜度是O(1)。
RemoveFirst
會將第一個元素以後的全部元素前移一個位置,Remove
也多是移除第一個元素,它們都是 n 的複雜度,而 RemoveLast
只須要移除最後一個元素。
Find
類的操做都須要遍歷數組,爲 n 的複雜度。Get
能夠直接經過鍵得到值,這也是數組的優點所在。
能夠發現,數組對已知索引和數組尾部的操做都是 O(1) 的複雜度,咱們能夠將這些理解爲數組的優點,當咱們用數組實現一些功能就能夠充分利用這些優點。
例如,咱們用數組模擬一個棧的操做,若是用數組尾部模擬棧頂,入棧、出棧都是都是 O(1) 時間複雜度,這是很是高效的。可是將出入棧放到數組頭部,時間複雜度就變成 O(n) 了,差距是顯而易見。