Go语言实现动态数组

数组设计之初是在形式上依赖内存分配而成的,所以必须在使用前预先请求空间。这使得数组有以下特性:

  1. 请求空间以后大小固定,不能再改变(数据溢出问题);
  2. 在内存中有空间连续性的表现,中间不会存在其他程序需要调用的数据,为此数组的专用内存空间;
  3. 在旧式编程语言中(如有中阶语言之称的C),程序不会对数组的操作做下界判断,也就有潜在的越界操作的风险(比如会把数据写在运行中程序需要调用的核心部分的内存上)。

以上引用自维基百科

传统数组的局限性导致了动态数组的诞生。然而动态数组也不是使用动态的内存,依旧是一块连续的内存。那它是如何实现数组大小不固定的呢?原因是当超过数组容量时,程序将自动执行扩容操作:

  1. 重新开辟一块大小为当前数组容量两倍的内存
  2. 把原数组的数据拷贝到此内存空间
  3. 释放原数组的内存

本文将从概念上实现动态数组的扩容和缩容特性,并实现数组增删查改操作的方法。

查看 Github 代码

数组结构

我们自定义的数组结构 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?

如果在 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) 了,差距是显而易见。