一. 基础
- Go 中数组变量属于值类型(value type),当一个数组变量被赋值或者传递时,实际上会对数组底层的内存进行拷贝,为了避免这个问题,可以传递指向数组的指针
- 数组固定长度,缺少灵活性,所以大部分场景下会选择使用基于数组构建的切片类型
languages := []string{"Go", "Python", "C"}
//也可以用内部提供的make函数初始化切片
//T 即元素类型,
//第二个参数是长度 len,即初始化的切片拥有多少个元素,
//第三个参数是容量 cap,容量是可选参数,默认等于长度
func make([]T, len, cap) []T
二. 操作切片与性能
copy
Append 与容量
- 切片底层有ptr指针,len长度,cap容量三个属性,在执行append追加 时有两种场景
- 当 append 之后的长度小于 cap,将会直接利用原底层数组剩余的空间。
- 如果新添加元素之后,slice 的长度大于等于底层数组的容量,会触发扩容
如果扩容前的容量小于 1024,那么新的容量将会扩大为原来的两倍
如果扩容前的容量大于等于 1024,那么新的容量将会扩大为原来的 1.25 倍
- 因此,为了避免内存发生拷贝,能够预知切片的最终大小时,可以预先设置 cap 的值
Delete
- 切片的底层是数组,因此删除意味着后面的元素需要逐个向前移位。每次删除的复杂度为 O(N),因此切片不合适大量随机删除的场景,这种场景下适合使用链表
- 优化的做法
//1.使用内置函数 copy 进行删除
func deleteElement(s []int, index int) []int {
//copy 可以将一个较长的 slice 拷贝到一个较短的 slice 中
copy(s[index:], s[index+1:])
//返回元素个数
return s[:len(s)-1]
}
//2.遍历切片进行删除
func deleteElement(s []int, index int) []int {
for i := index; i < len(s) - 1; i++ {
s[i] = s[i+1]
}
return s[:len(s)-1]
}
//3.将要删除的元素和最后一个元素交换位置,然后再将末尾元素截掉
func deleteElement(s []int, index int) []int {
s[index], s[len(s)-1] = s[len(s)-1], s[index]
return s[:len(s)-1]
}
- 并且手动将删除位置置空,有助于垃圾收集
Insert
insert 和 append 类似。即在某个位置添加一个元素后,将该位置后面的元素再 append 回去。复杂度为 O(N)。因此,不适合大量随机插入的场景
Filter
在过滤获取元素时,如果原切片不会再被使用,就地 filter 方式是比较推荐,可以节省内存
func filterSlice(s []int) []int {
i := 0
for _, v := range s {
if v >= 5 {
s[i] = v
i++
}
}
return s[:i]
}
Push
- 在末尾追加元素,不考虑内存拷贝的情况,复杂度为 O(1)
- 在头部追加元素,时间和空间复杂度均为 O(N),不推荐
Pop
- 尾部删除元素,复杂度 O(1)
- 头部删除元素,如果使用切片方式,复杂度为 O(1)。但是需要注意的是,底层数组没有发生改变,第 0 个位置的内存仍旧没有释放。如果有大量这样的操作,头部的内存会一直被占用
二. 切片的性能陷阱
- 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组。因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。比较推荐的做法,使用 copy 替代 re-slice,如下取 origin 切片的最后 2 个元素
func lastNumsBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
func lastNumsByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}