首页 > 其他分享 >数组和切片

数组和切片

时间:2023-03-20 10:06:02浏览次数:26  
标签:容量 s2 切片 数组 长度 底层


我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型。数组和切片有时候会让初学者感到困惑。它们的共同点是都属于集合类的类型,它们的值也都可以用来存储某一种类型的值(或者说元素)。

不过,它们最重要的不同是:数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。

数组的长度在声明它的时候就必须给定,并且之后不会再改变。可以说,数组的长度是其类型的一部分。比如,​​[1]string​​​和​​[2]string​​就是两个不同的数组类型。

而切片的类型字面量中只有元素的类型,而没有长度。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减小。

我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

也正因为如此,Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。

注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。

如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

我们在数组和切片之上都可以应用索引表达式,得到的都会是某个元素。我们在它们之上也都可以应用切片表达式,也都会得到一个新的切片。

我们通过调用内建函数​​len​​​,得到数组和切片的长度。通过调用内建函数​​cap​​,我们可以得到它们的容量。

但要注意,数组的容量永远等于其长度,都是不可变的。切片的容量却不是这样,并且它的变化是有规律可寻的。

下面我们就通过一道题来了解一下。我们今天的问题就是:怎样正确估算切片的长度和容量?

为此,我编写了一个简单的命令源码文件 demo15.go。

package main

import "fmt"

func main() {
// 示例 1。
s1 := make([]int, 5)
fmt.Printf("The length of s1: %d\n", len(s1))
fmt.Printf("The capacity of s1: %d\n", cap(s1))
fmt.Printf("The value of s1: %d\n", s1)
s2 := make([]int, 5, 8)
fmt.Printf("The length of s2: %d\n", len(s2))
fmt.Printf("The capacity of s2: %d\n", cap(s2))
fmt.Printf("The value of s2: %d\n", s2)
}

我描述一下它所做的事情。首先,我用内建函数​​make​​​声明了一个​​[]int​​​类型的变量​​s1​​​。我传给​​make​​​函数的第二个参数是​​5​​​,从而指明了该切片的长度。我用几乎同样的方式声明了切片​​s2​​​,只不过多传入了一个参数​​8​​以指明该切片的容量。

现在,具体的问题是:切片​​s1​​​和​​s2​​的容量都是多少?

这道题的典型回答:切片​​s1​​​和​​s2​​​的容量分别是​​5​​​和​​8​​。

问题解析

解析一下这道题。​​s1​​​的容量为什么是​​5​​​呢?因为我在声明​​s1​​​的时候把它的长度设置成了​​5​​​。当我们用​​make​​​函数初始化切片时,如果不指明其容量,那么它就会和长度一致。如果在初始化时指明了容量,那么切片的实际容量也就是它了。这也正是​​s2​​​的容量是​​8​​的原因。

我们顺便通过​​s2​​​再来明确下长度、容量以及它们的关系。我在初始化​​s2​​代表的切片时同时指定了它的长度和容量。

我们说过,可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。

在这种情况下,切片的容量实际上代表了它的底层数组的长度,这里是​​8​​。注意,切片的底层数组等同于我们前面讲到的数组,其长度不可变。

现在你需要跟着我一起想象:有一个窗口,你可以通过这个窗口看到一个数组,但是不一定能看到该数组中的所有元素,有时候只能看到连续的一部分元素。

现在,这个数组就是切片​​s2​​​的底层数组,而这个窗口就是切片​​s2​​​本身。​​s2​​​的长度实际上指明的就是这个窗口的宽度,决定了你透过​​s2​​​,可以看到其底层数组中的哪几个连续的元素。由于​​s2​​​的长度是​​5​​,所以你可以看到底层数组中的第 1 个元素到第 5 个元素,对应的底层数组的索引范围是 [0, 4]。

切片代表的窗口也会被划分成一个一个的小格子,就像我们家里的窗户那样。每个小格子都对应着其底层数组中的某一个元素。

我们继续拿​​s2​​​为例,这个窗口最左边的那个小格子对应的正好是其底层数组中的第一个元素,即索引为​​0​​​的那个元素。因此可以说,​​s2​​​中的索引从​​0​​​到​​4​​​所指向的元素恰恰就是其底层数组中索引从​​0​​​到​​4​​代表的那 5 个元素。

请记住,当我们用​​make​​​函数或切片值字面量(比如​​[]int{1, 2, 3}​​)初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素。

但是当我们通过切片表达式基于某个数组或切片生成新切片的时候,情况就变得复杂起来了。我们再来看一个例子:

s3 := []int{1, 2, 3, 4, 5, 6, 7, 8}
s4 := s3[3:6]
fmt.Printf("The length of s4: %d\n", len(s4))
fmt.Printf("The capacity of s4: %d\n", cap(s4))
fmt.Printf("The value of s4: %d\n", s4)

切片​​s3​​​中有 8 个元素,分别是从​​1​​​到​​8​​​的整数。​​s3​​​的长度和容量都是​​8​​​。然后,我用切片表达式​​s3[3:6]​​​初始化了切片​​s4​​​。问题是,这个​​s4​​的长度和容量分别是多少?

这并不难,用减法就可以搞定。首先你要知道,切片表达式中的方括号里的那两个整数都代表什么。我换一种表达方式你也许就清楚了,即:[3, 6)。

这是数学中的区间表示法,常用于表示取值范围,我其实已经在本专栏用过好几次了。由此可知,​​[3:6]​​​要表达的就是透过新窗口能看到的​​s3​​​中元素的索引范围是从​​3​​​到​​5​​​(注意,不包括​​6​​)。

这里的​​3​​​可被称为起始索引,​​6​​​可被称为结束索引。那么​​s4​​​的长度就是​​6​​​减去​​3​​​,即​​3​​​。因此可以说,​​s4​​​中的索引从​​0​​​到​​2​​​指向的元素对应的是​​s3​​​及其底层数组中索引从​​3​​​到​​5​​的那 3 个元素。

再来看容量。我在前面说过,切片的容量代表了它的底层数组的长度,但这仅限于使用​​make​​函数或者切片值字面量初始化切片的情况。更通用的规则是:一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。

由于​​s4​​​是通过在​​s3​​​上施加切片操作得来的,所以​​s3​​​的底层数组就是​​s4​​​的底层数组。又因为,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾。所以,​​s4​​​的容量就是其底层数组的长度​​8​​​, 减去上述切片表达式中的那个起始索引​​3​​​,即​​5​​。

注意,切片代表的窗口是无法向左扩展的。也就是说,我们永远无法透过​​s4​​​看到​​s3​​中最左边的那 3 个元素。

最后,顺便提一下把切片的窗口向右扩展到最大的方法。对于​​s4​​​来说,切片表达式​​s4[0:cap(s4)]​​​就可以做到。我想你应该能看懂。该表达式的结果值(即一个新的切片)会是​​[]int{4, 5, 6, 7, 8}​​​,其长度和容量都是​​5​​。

知识扩展

1. 问题:怎样估算切片容量的增长?

一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下简称原容量)的 2 倍。

但是,当原切片的长度(以下简称原长度)大于或等于​​1024​​​时,Go 语言将会以原容量的​​1.25​​​倍作为新容量的基准(以下新容量基准)。新容量基准会被调整(不断地与​​1.25​​相乘),直到结果不小于原长度与要追加的元素数量之和(以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。

另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准。注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。更多细节可参见​​runtime​​​包中 slice.go 文件里的​​growslice​​及相关函数的具体实现。

我把展示上述扩容策略的一些例子都放到了 demo16.go 文件中。你可以去试运行看看。

2. 问题:切片的底层数组什么时候会被替换?

确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它是把新的切片作为了新底层数组的窗口,而没有对原切片及其底层数组做任何改动。

请记住,在无需扩容时,​​append​​​函数返回的是指向原底层数组的新切片,而在需要扩容时,​​append​​函数返回的是指向新底层数组的新切片。所以,严格来讲,“扩容”这个词用在这里虽然形象但并不合适。不过鉴于这种称呼已经用得很广泛了,我们也没必要另找新词了。

顺便说一下,只要新长度不会超过切片的原容量,那么使用​​append​​函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。你可以运行 demo17.go 文件以增强对这些知识的理解。

总结

总结一下,我们今天一起探讨了数组和切片以及它们之间的关系。切片是基于数组的,可变长的,并且非常轻快。一个切片的容量总是固定的,而且一个切片也只会与某一个底层数组绑定在一起。

此外,切片的容量总会是在切片长度和底层数组长度之间的某一个值,并且还与切片窗口最左边对应的元素在底层数组中的位置有关系。那两个分别用减法计算切片长度和容量的方法你一定要记住。

另外,​​append​​函数总会返回新的切片,而且如果新切片的容量比原切片的容量更大那么就意味着底层数组也是新的了。还有,你其实不必太在意切片“扩容”策略中的一些细节,只要能够理解它的基本规律并可以进行近似的估算就可以了。

标签:容量,s2,切片,数组,长度,底层
From: https://blog.51cto.com/course/6131799

相关文章

  • Java基础语法-数组
    第一部分:数组1.数组1.1数组介绍数组就是存储数据长度固定的容器,存储多个数据的数据类型要一致。1.2数组的定义格式1.2.1第一种格式数据类型[]数组名示例:int[]arr......
  • Byte和byte[]数组
    https://www.cnblogs.com/cuihongyu3503319/p/5031670.htmlhttps://c.runoob.com/front-end/693/......
  • 数组总结
    1.数组的创建1.1字面量创建(常用)vara=[3,11,8];1.2构造器实际上newArray===Array,加不加new一点影响都没有。vara=Array();//[]创建一个数据的数组......
  • PHP 将空数组统一 json 序列化为 [] 的弊端
    在PHP中表示空的map或空数组都是以空数组形式,在转化为json数据时,会将空数组统一json序列化成 ​​[]​​,这样就存在一个类型问题。以前我们在与前端交互时一般是与弱类......
  • 判断一个数字在数组中是否存在,并返回---Java
    packagepractice.people.apple;//在数组中查找一个数,看是否存在,请返回值publicclassFoundNumber{publicstaticvoidmain(String[]args){//定义数组intar......
  • #yyds干货盘点# LeetCode面试题:最大子数组和
    1.简述:给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。 示例1:输入:nums=[-2,1,-3,4,-......
  • 字符与字符数组与字符串
    字符是一种系统自带的数据类型,用char定义,一次只能储存一个字符1#include<stdio.h>23intmain()4{5//定义一个字符变量,存储一个字符6charch......
  • Array数组
    数组: 数组是相同类型数据的有序集合 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成 每一个数据称作一个数组元素,每个数组元素都可以通过......
  • 384.打乱数组
    打乱数组给你一个整数数组nums,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。实现Solutionclass:Solution(int[]nums)使用整......
  • 如果获取VBA数组的维数
    如何用VBA代码获得数组具有多少维数使用以下自定义函数即可:PublicFunctionNumberOfDimensions(ByRefarrRefAsVariant)AsIntegerDimDimCountAsByte,j%......