• [golang]语法基础之切片


    说明

    切片是go语言当中的一种数据结构,整体来说,与前面说到的数组非常类似,但是相对于数组来说切片更加的灵活,原因在于切片的设计主要围绕着动态数组的概念设计的。可以根据需要自动更改大小,使用切片这种数据结构,可以比数组更加方便的管理和使用数据集合。

    内部实现

    从本质来说,切片是基于数组实现的,底层是数组,且切片本身非常小,可以理解为对底层的数组的抽象。也正是因为这样的原因,切片的底层和数组一样同样是连续匹配的,所以切片的效率也同样是极高的。同时和数组相同可以通过索引值获得数据,也可以进行迭代等操作。

    切片对象之所以小的原因是因为它是一个只有三个字段的数据结构:

    • 一个是指向底层数组的指针
    • 一个是切片的长度
    • 一个是切片的容量

    这三个字段,就是go语言操作底层数组的元数据,通过这三个字段,我们可以任意的操作切片。

    声明和初始化

    在go语言当中,切片的创建方式有多种,我们逐一的来说。

    首先,来说最简单的声明方式,通过make方式来创建。

    slice := make([]int,5)
    

    使用内置的make函数时,需要传入一个参数,指定切片的长度,上述代码中的数字5表示的就是切片的长度,需要知道的是,此时切片的容量也是5,在go语言当中也允许单独指定切片的容量。

    s1 := make([]int , 5 , 10 )
    

    上述代码中,切片的长度是5,容量是10(这个容量10对应的是切片底层数组的)。

    上面我们说到切片的本质其实是一个数组,所以我们在创建切片的时候,如果没有指定字面值的话,默认值就是数组元素的零值。

    上面的代码中,我们设置了容量是10,但是我们实际只能访问5个元素,因为切片的长度被设置为5,如果想要访问剩下的元素,需要先进行扩充。切片扩充之后才可以访问。

    需要注意的是容量必须>=长度,不予许创建长度大于容量的切片。

    还有一种创建切片的方式,是通过字面量,就是指定初始化的值。

    s1 :=[]int{1,2,3,4,5}
    

    上面的这种创建方式和数组非常类似,但是不需要定义[]当中的值,上面的这种写法切片的长度和容量都是相等,会根据我们指定的字面量推导出来。

    我们也可以像数组那样在切片创建的时候初始化切片某个索引的值:

    slice:=[]int{4:1}
    

    上面的代码中指定了第五个元素为1,其他元素都是默认值0.这时候切片的长度和容量也是一样的。

    一定要注意,创建数组时必须要设置长度或者设置为...

    //数组
    array:=[5]int{4:1}
    //切片
    slice:=[]int{4:1}
    

    切片还有nil切片和空切片,它们的长度和容量都是0,但是它们指向底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是个地址。

    如下:

    //nil切片
    var nilSlice []int
    //空切片
    slice:=[]int{}
    

    一般来说,nil切片表示不存在的切片,而空切片表示一个空集合,它们各有用处。

    切片另外一个用处比较多的创建是基于现有的数组或者切片创建。

    slice := []int{1, 2, 3, 4, 5}
    slice1 := slice[:]
    slice2 := slice[0:]
    slice3 := slice[:5]
    
    fmt.Println(slice1)
    fmt.Println(slice2)
    fmt.Println(slice3)
    

    基于现有的切片或者数组创建新的切片,使用[i:j]这样的操作符即可。表示从i索引值开始,到j索引值结束,截取原数组或者切片,创建而成的新切片,新切片的值包含原切片的i索引,但是不包含j索引。

    i如果省略,默认是0;j如果省略默认是原数组或者切片的长度,所以上述代码中的三个切片的值是一样的。需要知道的是,这里的i和j都不能超过原切片或者数组的索引。

    slice := []int{1, 2, 3, 4, 5}
    newSlice := slice[1:3]
    
    newSlice[0] = 10
    	
    fmt.Println(slice) // [1 10 3 4 5]
    fmt.Println(newSlice) // [10 3]
    

    我们可以通过上述的代码看到,新的切片和原切片共用的是一个底层数组,所以当修改的时候,底层数组的值会被改变,所以原切片的值也就被改变了。同理,对于基于数组的切片也是一样的。

    如果基于原数组或者切片创建一个新的切片后,那么新的切片的大小和容量是多少呢?

    可以参照下面的公式:

    对于底层数组容量是k的切片slice[i:j]来说
    长度:j-i
    容量:k-i
    

    比如上面的例子当中,slice[1:3] ,长度是3-1=2,容量是5-1=4。看上去有些麻烦对吗?

    如果在代码中想要获取长度和容量,go语言给我们提供了len()函数用来计算切片的长度,提供了cap()函数用来计算切片的容量。

    slice := []int{1,2,3,4,5}
    s := slice[1:3]
    fmt.Printf("切片s的长度是%d,容量是%d 
    ",len(s),cap(s))
    

    以上是基于一个数组或者切片使用两个索引创建新切片的方法,除此之外还有一种通过三个索引值创建切片的方法,第三个用来限定新切片的容量。

    语法格式为:

    slice[i:j:k]
    

    例如:

    slice := []int{1, 2, 3, 4, 5}
    newSlice := slice[1:2:3]
    

    在上面的代码中,我们创建了一个长度为2-1=1,容量为3-1=2的新切片,不过需要注意的是,第三个索引不能超过原切片的最大索引值5。

    使用切片

    去使用一个切片,和使用数组一样,都是通过索引值就可以获取切片相对应的元素的值,同样也可以修改对应元素的值。

    例如:

    s1 := []int{1,2,3,4,5}
    fmt.Println(s1[2]) // 获取值  3 
    s1[2] = 10 // 修改值
    fmt.Println(s1[2]) // 输出10 
    

    切片只能访问到其长度内的元素,访问超过长度外的元素,会导致运行时异常,与切片容量关联的元素只能用于切片增长。

    在上面我们说过,切片是一个动态的数组,可以实现按需增长,我们可以使用go内置的append函数实现切片的增长。

    append函数可以为切片追加一个元素。

    例如:

    s1 := []int{1,2,3,4,5}
    s2 := s1[0:2]
    
    // 通过append方法想s2中追加
    s2 = append(s2,9)
    fmt.Println(s2) // [1 2 9]
    fmt.Println(s1) // [1 2 9 4 5]
    

    在上面的代码中,我们通过append函数为新创建的切片s2追加了一个元素9,当我们通过打印后可以发现,原切片s1索引值为2的位置上的值也被改变了,变成了9。

    引起这种结果的原因是因为newSlice有可用的容量,不会创建新的切片来满足追加,所以直接在newSlice后追加了一个元素10,因为newSlice和slice切片共用一个底层数组,所以切片slice的对应的元素值也被改变了。

    这里newSlice新追加的第3个元素,其实对应的是slice的第4个元素,所以这里的追加其实是把底层数组的第4个元素修改为10,然后把newSlice长度调整为3。

    需要知道的是,如果当切片的底层数组,没有足够的容量时,就会新建一个底层数组,把原来数组的值复制到新底层数组里,再追加新值,这时候就不会影响原来的底层数组了。

    所以一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题,因为共用数组的时候修改内容,会影响多个切片。

    append函数会智能的增长底层数组的容量,目前的算法是:容量小于1000个时,总是成倍的增长,一旦容量超过1000个,增长因子设为1.25,也就是说每次会增加25%的容量。

    内置的append函数也是一个可变参数的函数,所以我们可以同时追加几个值。

    s2 = append(s2,10,20,30)
    

    此外,我们还可以通过...操作符,把一个切片追加到另外一个切片里。

    s1 := []int{1,2,3,4,5}
    s2 := s1[1:2:3]
    
    s2 = append(s2,s1...)
    fmt.Println(s2)
    fmt.Println(s1)
    

    迭代切片

    既然切片是一个集合,我们可以使用for range 循环来迭代它,打印其中的每个元素以及对应的索引。

    slice := []int{1, 2, 3, 4, 5}
    for i,v:=range slice{
    	fmt.Printf("索引:%d,值:%d
    ",i,v)
    }
    

    如果我们不想要索引,可以使用_来忽略它,这时go的用法,很多不需要的函数返回值,都可以忽略。

     slice := []int{1, 2, 3, 4, 5}
    for _,v:=range slice{
    	fmt.Printf("值:%d
    ",v)
    }
    

    这里需要说明的是range返回的是切片元素的复制,而不是元素的引用

    除了for range循环外,我们也可以使用传统的for循环,配合内置的len函数进行迭代。

    slice := []int{1, 2, 3, 4, 5}
    for i := 0; i < len(slice); i++ {
    	fmt.Printf("值:%d
    ", slice[i])
    }
    

    在函数间传递切片

    我们知道切片是3个字段构成的结构类型,所以在函数间以值的方式传递的时候,占用的内存非常小,成本很低。在传递复制切片的时候,其底层数组不会被复制,也不会受影响,复制只是复制的切片本身,不涉及底层数组。

    func main() {
    	slice := []int{1, 2, 3, 4, 5}
    	fmt.Printf("%p
    ", &slice)
    	modify(slice)
    	fmt.Println(slice)
    }
    
    func modify(slice []int) {
    	fmt.Printf("%p
    ", &slice)
    	slice[1] = 10
    }
    

    打印输出结果如下:

    0xc420082060
    0xc420082080
    [1 10 3 4 5]
    

    通过上面的结果可以看出,这两个切片的地址并不一样,所以可以确认切片在函数间传递是复制的。而我们修改一个索引的值后,发现原切片的值也被修改了,说明它们共用一个底层数组。

    在函数间传递切片非常高效,而且不需要传递指针和处理复杂的语法,只需要复制切片,然后根据自己的业务修改,最后传递回一个新的切片副本即可,这也是为什么函数间传递参数,使用切片,而不是数组的原因。

  • 相关阅读:
    PAT甲题题解1099. Build A Binary Search Tree (30)-二叉树遍历
    PAT甲题题解1098. Insertion or Heap Sort (25)-(插入排序和堆排序)
    PAT甲级题解-1097. Deduplication on a Linked List (25)-链表的删除操作
    字典树trie的学习与练习题
    POJ 1273 Drainage Ditches(网络流dinic算法模板)
    单元最短路径算法模板汇总(Dijkstra, BF,SPFA),附链式前向星模板
    Hungary(匈牙利算法)——二分图最大匹配
    LCIS 最长公共上升子序列
    POJ 1151 Atlantis(经典的线段树扫描线,求矩阵面积并)
    Codeforces 494D Upgrading Array
  • 原文地址:https://www.cnblogs.com/liujunhang/p/12534598.html
Copyright © 2020-2023  润新知