Golang动态数组的实现示例
作者:勇者无畏404
什么是动态数组
动态数组(Dynamic Array)是一种在需要时能够自动改变其大小的数组。与静态数组(Static Array)不同,静态数组的大小在声明时就已确定,且之后不能更改,而动态数组的大小则是根据需要动态变化的。动态数组通常通过某种数据结构(如链表、数组加额外信息等)来实现,以便在需要时能够扩展或缩小其容量。(关键字:动态扩缩容,基于链表或数组实现)
在大多数现代编程语言中,如C++的std::vector
、Java的ArrayList
、Python的列表(list)等,都提供了动态数组的功能。这些实现通常会在内部使用一个静态数组来存储元素,并跟踪当前数组的大小和已分配的容量。当向动态数组添加元素而当前容量不足时,它们会自动分配一个新的、更大的数组,并将旧数组的元素复制到新数组中,然后再添加新元素。这个过程对用户是透明的,用户无需关心底层的内存管理细节。
Go语言中的动态数组——切片(slice)
Go语言(Golang)中的切片(slice)是一种引用类型,它提供了对数组的抽象。切片是对数组一个连续片段的引用,但它比数组更灵活、更强大。以下是Go语言切片的一些主要特点:
动态数组:切片的大小不是固定的,它们可以根据需要增长和缩小。当向切片中添加更多元素,且切片容量不足时,Go会自动分配更大的内存空间并复制原有元素到新空间中,从而允许切片继续增长。
引用类型:切片是引用类型,这意味着切片变量存储的是对底层数组的引用(即内存地址),而不是数组的拷贝。因此,通过不同的切片变量操作同一个底层数组的元素时,这些操作会相互影响。
长度和容量:切片有两个重要的属性:长度(length)和容量(capacity)。长度是切片中元素的数量;容量是从切片的第一个元素开始到底层数组末尾的元素数量。切片的长度可以改变,但容量在切片创建时由底层数组的大小决定,且一般只能通过重新切片或使切片指向一个新的数组来改变。
基于数组:切片是对数组的一个连续片段的引用,但切片的使用比数组更加灵活和方便。切片可以动态地增长和缩小,而数组的大小在定义时就确定了,不能改变。
零值:切片的零值是
nil
,表示切片不引用任何数组。一个nil
切片的长度和容量都是0,并且没有底层数组。切片操作:Go支持对切片进行切片操作,即可以从一个已存在的切片中再“切”出一个新的切片。这种操作基于原切片的底层数组,但可以有不同的长度和容量。
内存连续:切片的底层数组在内存中是连续的,这使得对切片中的元素进行迭代访问时效率很高。
内置函数支持:Go的标准库提供了许多内置函数来操作切片,如
append
用于向切片追加元素,copy
用于切片间的元素复制,以及len
和cap
用于获取切片的长度和容量等。
切片数据结构
type slice struct { // 底层数组指针(指向一块连续内存空间的起点) array unsafe.Pointer // 切片长度 len int // 切片容量 cap int }
cap>=len
切片的创建和初始化
//通过字面量创建 /** 创建字符串切片,长度和容量都是3 */ slice1 := []string{"aaa","bbb","ccc"} fmt.Println(slice1) //[aaa bbb ccc] /** 创建整形切片,长度和容量都是4 */ slice2 := []int{10,20,30,40} fmt.Println(slice2) //[10 20 30 40] //当使用切片字面量创建切片时,还可以设置初始长度和容量。 /** 创建一个长度和容量都是100的切片 索引0 = 10,索引99 = 1 */ slice3 := []int{0:10,99:1} fmt.Println(slice3) //[10 0 0 ... 1] //通过 make() 函数创建切片 /** 创建长度和容量都是5的切片 */ slice4 := make([]int,5) fmt.Println(slice4) //[0 0 0 0 0] 默认对应类型的零值 /** 创建长度=3,容量=5的切片 */ slice5 := make([]string,3,5) fmt.Println(slice5) //[ ] /** 不允许创建容量小于长度的切片 */ slice6 := make([]int,5,3) fmt.Println(slice6) //invalid argument: length and capacity swapped
切片初始化源码
func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } return mallocgc(mem, et, true) }
- math.MulUintptr 方法计算出初始化切片需要的内存和空间大小
- 空间超限,直接panic
- mallocgc方法,为切片进行空间分配
切片在方法之间的传递
func main() { sl := []int{1,2,3,4,5} fmt.Println(sl) //[1 2 3 4 5] change(sl) fmt.Println(sl) //[10 2 3 4 5] } func change(sl []int) { sl[0] = 10 }
每次在方法之间传递切片时,会将切片实例本身进行一次值拷贝(也可以叫浅拷贝),会将array指针,len长度,cap容量进行拷贝所以方法中和方法外的切边array指向同一片内存空间,因此在局部方法中执行修改操作时,还会根据这个地址信息影响到原 slice 所属的内存空间,从而对内容发生影响;
但是如果在函数中的切片发生了扩容,此时内外就是两个独立的切片;
截取切片
在Go语言中,可以通过指定起始索引和结束索引来截取一个新的切片(包前不包后);
slice[start:end] //slice要截取的原始切片 //start截取切片的开始位置,不填默认为0 //end截取切片的结束位置,不填默认为切片长度
s := []int{1, 2, 3, 4, 5} // 截取整个切片 subS1 := s[:] fmt.Println(subS1) // 输出: [1 2 3 4 5] // 截取索引1到4(不包含4)的元素 subS2 := s[1:4] fmt.Println(subS2) // 输出: [2 3 4] // 从索引0开始截取到索引4(不包含4) subS3 := s[:4] fmt.Println(subS3) // 输出: [1 2 3 4] // 从索引2开始截取到切片末尾 subS4 := s[2:] fmt.Println(subS4) // 输出: [3 4 5]
截取出的切片和原始切片的关系
在对切片 slice 执行截取操作时,本质上是一次引用传递操作,因为不论如何截取,底层复用的都是同一块内存空间中的数据,只不过,截取动作会创建出一个新的 slice header 实例;
s := []int{2,3,4,5} s1 := s[1:]
往切片中新增元素(append)
通过append方法可以实现在切片的末尾添加元素
func main() { s := []int{1, 2, 3} // 使用 append 函数在切片末尾添加元素 4 s = append(s, 4) fmt.Println(s) // 输出: [1 2 3 4] } func main() { s := []int{1, 2, 3} // 使用 append 函数在切片末尾添加多个元素 s = append(s, 4, 5, 6) fmt.Println(s) // 输出: [1 2 3 4 5 6] } func main() { s1 := []int{1, 2, 3} s2 := []int{4, 5, 6} // 使用 append 函数将 s2 添加到 s1 的末尾 s1 = append(s1, s2...) // 注意 s2 后的 ... 用于将 s2 展开为元素序列 fmt.Println(s1) // 输出: [1 2 3 4 5 6] } func main() { s := []int{1, 3, 4, 5} // 在索引 1 的位置插入元素 2 // 首先,将索引 1 及其之后的元素向后移动一位 s = append(s[:1], append([]int{2}, s[1:]...)...) fmt.Println(s) // 输出: [1 2 3 4 5] }
删除切片中的元素
在Go语言中,切片(slice)本身并不提供直接的删除元素的方法,因为切片是对底层数组的抽象,而数组的大小是固定的。但是,通过以下几种方式来实现“删除”切片中的元素;
//使用切片操作删除元素(适用于删除末尾元素) s := []int{1, 2, 3, 4, 5} // 删除切片末尾的元素 s = s[:len(s)-1] fmt.Println(s) // 输出: [1 2 3 4] //使用append和切片操作删除任意位置的元素 s := []int{1, 2, 3, 4, 5} // 假设我们要删除索引为2的元素(值为3) index := 2 // 将index之后的元素(不包括index)复制到前面,然后截断切片 s = append(s[:index], s[index+1:]...) fmt.Println(s) // 输出: [1 2 4 5] //使用循环和条件语句删除满足条件的元素 s := []int{1, 2, 3, 4, 5} // 删除所有值为3的元素 var newS []int for _, value := range s { if value != 3 { newS = append(newS, value) } } s = newS fmt.Println(s) // 输出: [1 2 4 5]
拷贝切片
slice 的拷贝可以分为简单拷贝和完整拷贝两种类型;
要实现简单拷贝(浅拷贝),我们只需要对切片的字面量进行赋值传递即可,这样相当于创建出了一个新的 slice header 实例,但是其中的指针 array、容量 cap 和长度 len 仍和老的 slice header 实例相同;
slice 的完整复制(深拷贝),指的是会创建出一个和 slice 容量大小相等的独立的内存区域,并将原 slice 中的元素一一拷贝到新空间中,在实现上,slice 的完整复制可以调用系统方法 copy;
s := []int{1, 2, 3} // 分配一个新的切片,长度和容量与s相同 sCopy := make([]int, len(s)) // 使用copy函数复制元素 copy(sCopy, s) // 现在sCopy是s的一个独立拷贝 fmt.Println(sCopy) // 输出: [1 2 3]
切片的扩容
什么时候会触发扩容
当 slice 当前的长度 len 与容量 cap 相等时,下一次 append 操作就会引发一次切片扩容;
扩容步骤
1.计算目标容量
case1:如果新切片的长度>旧切片容量的两倍,则新切片容量就为新切片的长度case2:
- 如果旧切片的容量小于256,那么新切片的容量就是旧切片的容量的两倍
- 反之需要用旧切片容量按照1.25倍的增速,直到>=新切片长度,为了更平滑的过渡,每次扩大1.25倍,还会加上3/4* 256
2.进行内存对齐
需要按照Go内存管理的级别去对齐内存,最终容量以这个为准
到此这篇关于Golang动态数组的实现示例的文章就介绍到这了,更多相关Golang动态数组内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!