动态数组slice

slice 又称动态数组,依托于数组实现,可以方便的进行扩容和传递,实际使用时比数组更灵活。但正是因为灵活,实际使用时更容易出错,避免出错的最好方法便是了解其实现原理。

特性速览

初始化

声明和初始化切片的方式主要有以下几种方式:

  • 变量声明
  • 字面量
  • 使用内置函数make()
  • 从切片和数组中切取
变量声明

这种声明方式的切片变量与声明其他类型变量一样,变量值都为零值,对于切片来讲零值为nil

var s []int
字面量

也可以使用字面量初始化切片,需要了解的是空切片是指长度为空,其值并不是nil。

声明长度为0的切片时,推荐使用变量声明的方式获得一个nil切片,而不是空切片,因为nil切片不需要内存分配。

s1:=[]int{} //空切片
s2:=[]int{1,2,3}   //长度为3的切片
内置函数make()

内置函数make() 可以创建切片,切片元素均初始化为相应类型的零值。

推荐指定长度的同时预估空间,可有效的减少切片扩容时内存分配以及拷贝次数。

s1:=make([]int,12)  //指定长度
s2:=make([]int,10,100) //指定长度和空间
切取

切片可以基于数组和切片创建,需要了解的是切片与原数组或者原切片共享底层空间,修改切片会影响原数组或者原切片。

切片表达式[low:high] 表示的是左闭右开[low,high)区间,切取的长度为 high - low。

array := [5]int{1,2,3,4,5}
s1 := array[0:2]   //从数组中切取
s2 := s1[0:1]          //从切片切取
fmt.Println(s1)     // [1,2]
fmt.Println(s2)     // [1]

另外,适用于任意类型的内置函数new() 也可以创建切片:

s := *new([]int)    //此时创建的切片值为nil

切片操作

内置函数append()用于向切片中追加元素:

s := make([]int,0)  //初始化切片
s = append(s,1)            //添加1个元素
s = append(s,2,3,4)            //添加多个元素
s = append(s,[]int{5,6}...)            //添加1个切片
fmt.Println(s)          //[1,2,3,4,5,6]

当切片空间不足时,append()会先创建新的大容量切片,添加元素后返回新切片。

内置函数len()和cap()分别用于查询切片的长度和容量,由于切片的本质为结构体,结构体中直接存储了切片的长度和容量,所以这两个函数的时间复杂度都为O(1)。

实现原理

slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足的时候可以 实现自动重分配并生产新的slice。

数据结构

源码包中src/runtime/slice.go:slice 定义了slice的数据结构:

type slice struct {array unsafe.Pointer  //底层数组len   int     //长度cap   int       //容量
}

从数据结构上看slice很清晰,array指针指向底层数组,len表示数组长度,cap表示底层数组的容量。

切片操作

使用make()创建slice

使用make()创建slice时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即为容量。

例如,slice := make([]int , 5 , 10)语句所创建的slice的结构如下图所示:

该slice的长度为5,即可以使用下标slice[0]~slice[4]来操作里面的元素,capacity为10,表示后续想slice添加新元素的时候可以不必重新分配内存,直接使用预留的内存即可,直到预留的内存不足再进行扩容。

使用数组创建slice

使用数组创建slice时,slice将于原数组共用一部分内存

例如,slice := array[5,7]语句所创建的slice的结构如下图所示:

切片从数组array[5]开始,到数组array[7]结束(不包含7),即切片的长度为2,数组后面的内存都作为切片的预留内存,即capacity为5。

数组和数组的切片共享底层存储空间,这是使用过程中需要额外注意的地方。如果切片是从数组中创建而来,那么对切片的操作会影响原始数组,如果有多个切片从同一个数组中创建,那么对一个切片的操作会影响其他切片。

slice扩容

使用append向slice追加元素时,如果slice空间不足,则会触发slice扩容,扩容实际上是重新分配一块更大的内存,将原slice的数据拷贝进新的slice中,然后返回新slice,扩容后再将数据追加进去。

例如,当向一个capacity为5,且length也为5的slice再次追加1个元素时,就会发生扩容,如下图所示:

扩容操作只关心容量,会把原slice的数据拷贝到新slice中,追加数据由append在扩容结束后进行。在上图中可以看出,扩容完成后,len还是5,但是cap由5变成了10,原slice的数据也拷贝到了新slice中了。

扩容容量的选择遵循一下基本规则:

  • 如果原slice的容量小于1024,则新slice的容量将扩容到2倍
  • 如果原slice的容量大于等于1024,则新slice的容量将扩容1.25倍

在该规则的基础上,还会考虑元素类型与内存分配规则,对实际扩展值做一些微调。从这个基本规则中可以看出Go对slice的性能和空间使用率的思考。

  • 当切片较小时,采用较大的扩容倍速,可以避免频繁扩容,从而减少内存分配次数和数据拷贝的代价
  • 当切片较大时,采用较小的扩容倍速,主要是为了避免浪费空间

使用append()向slice添加一个元素的实现步骤如下:

  • 加入slice的容量够用,直接追加进去,slice.len++,返回slice
  • 原slice的容量不够,将slice先扩容,扩容后得到新的slice
  • 将新元素追加进新slice中,slice.len++,返回新slice
slice拷贝

使用copy()内置函数拷贝两个切片时,会将原切片的数据逐个拷贝到目标切片指向的数组中,拷贝数量取决于两个切片长度的最小值。假如目标切片容量不够,不会发生扩容的情况。

小结

  • 每个切片都指向一个底层数组
  • 每个切片都保存了当前切片的长度和容量
  • 使用len()计算切片长度的时间复杂度为O(1)
  • 使用cat()计算切片容量的时间复杂度为O(1)
  • 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是一个结构体而已
  • 使用append()向切片追加元素时有可能会发生扩容现象,扩容后会生成新的切片

此处有几个值得注意的编程建议:

  • 创建切片时,如果可以估计到使用容量,尽可能的指定,可以避免在追加元素的过程中出现扩容操作,有利于提升性能
  • Copy()函数切片拷贝时需要判断实际拷贝的元素个数,有可能目标切片长度不足,产生丢失数据的情况
  • 谨慎使用多个切片操作同一个数组,以防出现读写冲突

切片表达式

slice表达式可以基于一个字符串生成子字符串,也可以从一个数组或者切片中生成切片。Go语言提供了两种表达式:

  • 简单表达式: array[low : high]
  • 扩展表达式: array[low : high : max]

简单表达式

简单表达式日常使用的频率最高,其格式为 array[low : high]。如果a为数组或者切片,则该表达式将切取 array位于[low : high)区间的元素并生成一个新的切片。如果array为字符串,稍微有一点特殊的是该表达式会生成一个字符串,而不是切片。

简单表达式生成的切片的长度为 high - low。例如我们使用简单表达式切取数组array并生成新的切片b:

array :=[5]int{1,2,3,4,5}
b := a[1:4]

此时得到的切片b的长度为3,元素分别为:

b[0] ==2
b[1] ==3
b[2] ==4
底层数组共享

根据之前介绍的切片的数据结构,我们知道每个切片包含三个元素:

type slice struct {array unsafe.Pointer  //底层数组len   int     //长度cap   int       //容量
}

这里需要着重强调的是,使用简单表达式生成的切片将于原数组或者切片共享底层数组。 新切片的生成逻辑可以使用以下伪代码表示:

b.array = &array[low]
b.len = high - low
b.cap = len(a) = low
边界问题

如果简单表达式切取的对象为字符串或者数组,那么在表达式 a[low : high] 中 low 和 high的取值需要满足以下关系

0 <= low <= high < len(a)

如果简单表达式切片的对象为切片,那么在表达式 a[low : high] 中 low 和 high 的最大取值可以为a的容量,而不是a的长度:

a <= low <= high <= cap(a)
切取string

表达式a[low : high]作用于数组、切片时会产生新的切片,作用于字符串时则会产生新的字符串,而不是切片。

这是由string和slice的类型差异决定的,slice可以支持随机读写,而string不可以。

默认值

为了使用方便,简单表达式 a[low : high] 中low 和high 都是可以省略的。

low默认值为0,而high的默认值为表达式作用对象的长度

a[:high] 等同于 a[0:high]
a[0:] 等同于 a[0:len(a)]
a[:] 等同于 a[0:len(a)]

扩展表达式

简单表达式生成的切片与原数组或者切片共享底层数组避免了拷贝元素,节约内存空间的同时,也会带来读写冲突的风险。

新切片b( b := a[low : high] ) 不仅可以读写a[low] 到 a[high-1]之间的元素,而且在使用append(b,x)函数增加新元素x时,还可能会覆盖a[high]以及后面的元素。例如:

a := [5]int{1,2,3,4,5}
b := a[1:4]
b = append(b,0)    //此时a[4]将由5变为0

使用新切片覆盖a[high]以及后面的元素,有可能是非预期的,从而产生灾难性的后果。

Go团队很早就关注到了这个风险,并且在Go1.2中就提供了一种可以限制新切片容量的表达式,即扩展表达式:

a[low : high : max]

扩展表达式中的max用于限制新生成切片的容量,新切片的容量为 max-low,表达式中low、high 和max的关系需要满足如下:

o <= low <= high <= max <= cap(a)

对于一个长度为10的数组,使用扩展表达式切取其中两个元素生成的新切片的拓扑结构如下图所示:

如果使用简单表达式,那么上图中切片的容量为5,而使用扩展表达式时切片的容量则被限制为2.

扩展表达式常见于偏底层的代码中,比如Go源码。扩展表达式生成的切片被限制存储容量,当使用append()函数向此切片追加元素时,如果容量不足则会产生一个全新的slice,而不会覆盖原数组或者切片。

扩展表达式中的a[low : high : max]只有low是可以省略的,其默认值为0。这一点与简单表达式不同。

如果缺失了 high 或者max, 则会产生编译错误。

小结

  • slice表达式分为简单表达式 a[low , high] 和扩展表达式 a[low : high : max]
  • 简单表达式作用于数组、切片时会产生新的切片,作用于字符串时会产生新的字符串
  • 扩展表达式只能作用于数组、切片,不用作用于字符串

golang数据结构初探之动态数组slice相关推荐

  1. golang数据结构初探之管道chan

    golang数据结构初探之管道chan 管道是go在语言层面提供的协程之间的通信方式,比unix的管道更易用也更轻便. 特效速览 初始化 声明和初始化管道的方式主要有以下两种: 变量声明 使用内置函数 ...

  2. 2021-9-下旬 数据结构-线性表-动态数组-java代码实现

    信管的数据结构讲的太水了,遂重学(看的网课:恋上数据结构与算法,讲的very good),为算法学习打基础(都大三了还在打基础),顺便在leetcode上跟进一些相关的题,记录在这里,这里最方便复习 ...

  3. java动态数组的实现的_Java实现数据结构之【动态数组】

    数组是学习编程语言时较先接触到的一种数据结构,本章基于Java的静态数组实现动态数组,并进行简单的复杂度分析 数组相信各位都知道,那什么是动态数组呢?我们定义一个数组后,一般长度会直接定义好,如果数组 ...

  4. 数据结构基础之动态数组

    动态数组 数组的局限性 目前为止所实现的数组类,有一个非常严重的局限性,就是这个数组实际使用的还是一个静态数组,内部容量有限.在实际使用的时候,我们往往无法预估要在这个数组中存入多少个元素. 解决方案 ...

  5. golang数据结构初探之字符串string

    字符串string string 是Go语言中的基础数据类型. 特性速览 声明 声明string变量非常简单,常见的方式有以下两种: 声明一个空字符串后再赋值 var s string s = &qu ...

  6. golang数据结构初探之字典map

    Map Go语言的map底层使用Hash表实现的 特性预览 操作方式 初始化 map分别支持字面量初始化和内置函数make()初始化 字面量初始化 func MapInit() {m := map[s ...

  7. golang数据结构与算法——稀疏数组、队列和链表

    文章目录 一 稀疏数组 1.1 先看一个实际的需求 1.2 稀疏数组基本介绍 1.3 稀疏数组举例说明 1.4 把数组转换为稀疏数组实现 1.5 把稀疏数组还原为原数组 二 队列 2.1 队列的介绍 ...

  8. 基于java的数据结构学习——泛型动态数组的封装

    public class Array<E> {private E[] data;private int size;// 构造函数public Array(int Capacity){dat ...

  9. 数据结构与算法 —— 动态数组

    一.基础与细节 1. 扩容策略 无论是 C++ STL 中的向量 vector 还是 Java Collections 中的 ArrayList,采用的扩容策略是_capacity <<= ...

最新文章

  1. 简单易用NLP框架Flair发布新版本!(附教程)
  2. element-ui中单独引入Message组件的问题
  3. delphi项目开发经验2008年09月18日 星期四 10:07随着项目的失败,这些天一直在总结失败的原因,到底是为什么?
  4. linux内核趣味,有关Linux 50个趣味名人名言
  5. 【MySQL】AUTO_INCREMENT只能应用于数值类型的列,且该列需要被索引
  6. 20165222第一周查漏补缺
  7. 【华为云技术分享】从 Cloud 1.0 到 2.0,云计算的“多元架构命题”
  8. 惠普台式机重装系统之后,无法进入系统
  9. java基于ssm的农产品网上销售系统
  10. 外星人 Alienware X14 评测
  11. 深度学习常用数据集汇总
  12. 游戏开发物理引擎PhysX研究系列:通过Unity中的物理系统学习Physx指引贴
  13. top 100 percent
  14. ijkplayer源码---倍速
  15. Oracle Sqlplus显示不足问题
  16. java毕业设计springboot框架 java二手交易网站系统毕业设计开题报告功能参考
  17. 4342. 就一勾子 HDU1698 , kuangbin专题
  18. 思岚RPLIDAR A2激光雷达使用及问题解决
  19. Linux 容器能否弥补 IoT 的安全短板?
  20. python小象学院: BMR------ 基础代谢率1.0

热门文章

  1. 三角形形状判断(等边、等腰、直角、等腰直角、非等边)
  2. IC卡读卡器调用php,IC卡读卡器如何进行IC卡的读卡?
  3. DROID-SLAM: Deep Visual SLAM for Monocular, Stereo, and RGB-D Cameras论文阅读笔记
  4. 【pandas数据分析】pandas数据结构
  5. 浙江大学招生目录新增一整个联合学院,包含人工智能,计算机专硕!
  6. 【uml】-九种图之活动图(Activity Diagram))
  7. TextView与EditText
  8. redis 运维讲解01
  9. 基于机器学习的心脏病预测方法(1)——心脏病及Heart Disease UCI数据集介绍
  10. 【JS】把JavaScript脚本作为书签收藏起来并可单击执行