zoukankan      html  css  js  c++  java
  • Go语言核心之美 3.2-slice切片

    Slice(切片)是长度可变的元素序列(与之相应,上一节中的数组是不可变的),每一个元素都有同样的类型。slice类型写作[]T。T是元素类型。slice和数组写法非常像,差别在于slice没有指定长度。

    数组和slice之间的联系是非常紧密的。

    slice是非常轻量的数据结构,是引用类型,它指向一个底层数组,该数组被称之为slice的底层数组,slice能够訪问底层数组的某个子序列。也能够訪问整个数组。

    一个slice由三个部分组成:指针、长度、容量。指针指向了slice中第一个元素相应的底层数组元素的地址,由于slice未必是从数组第一个元素開始,因此slice中的第一个元素未必是数组中的第一个元素;长度是slice中的元素数目。是不能超过容量的。容量通常是从slice中第一个元素相应底层数组中的開始位置,究竟层数组的结尾的长度。内置函数len和cap分别返回slice的长度和容量。

    多个slice能够共享底层数组,甚至它们引用的部分能够相互重叠。图4.1表示了一个数组。它的元素是一年中各个月份的字符串,还有两个重叠引用了底层数组的slice。数组定义:

    months := [...]string{1: "January", /* ... */, 12: "December"}
    

    当中一月份是months[1],十二月是months[12]。通常来说。数组第一个元素的索引从0開始。可是月份通常是从1月開始到12月。因此在声明数组时,我们跳过了第0个元素。这里,第0个元素会被默认初始化为""(空字符串)。

    以下介绍切片操作s[i:j]。当中0 ≤ i≤ j≤ cap(s),该操作会创建一个新的slice,slice会引用s中从第i个元素到第j-1个元素。新的slice有j-i个元素。假设省略下标i,写成s[:j],实际上代表s[0:j]。假设省略下标j。写成s[i:],代表s[0:len(s)]。因此months[1:13]操作将引用全部月份,和months[1:]操作等价。

    months[:]则是引用整个数组。

    以下分别表示第二个季度和北方的夏天:

    Q2 := months[4:7]
    summer := months[6:9]
    fmt.Println(Q2)     // ["April" "May" "June"]
    fmt.Println(summer) // ["June" "July" "August"]
    

    两个slice有重叠部分:六月份。以下是一个是否包括同样月份的測试(性能不高):

    for _, s := range summer {
        for _, q := range Q2 {
            if s == q {
                fmt.Printf("%s appears in both
    ", s)
            }
        }
    }
    

    假设slice操作訪问的下标大于cap(s)将导致panic(越界),假设超过len(s)则意味着扩展slice(不能超过cap(s) ):

    fmt.Println(summer[:20]) // panic: out of range
    
    endlessSummer := summer[:5] // 在容量同意内扩展summer
    fmt.Println(endlessSummer)  // "[June July August September October]"
    

    另外。字符串的slice操作和[]byte的slice操作是非常相似的,它们都表现为x[m:n],而且都返回原始序列的子序列。底层数组也都是原始序列,因此slice操作是常量级别的时间复杂度(仅仅是更新slice中的指向位置。长度,容量)。

    若x[m:n]的目标是字符串,则生成一个子串;若目标是[]byte,则生成新的[]byte。

    由于slice引用包括了指向底层数组的指针。因此向函数传递slice后,函数能够在内部对slice的底层数组进行更改。换而言之。复制slice就是为底层数组创建一个新的引用,代价是非常低的(事实上就是复制一个含有三个字段的struct)。以下的reverse函数对[]int类型的slice进行就地反转(无内存分配),它能够用于随意长度的slice:

    // reverse reverses a slice of ints in place.
    func reverse(s []int) {
        for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
            s[i], s[j] = s[j], s[i]
        }
    }
    

    反转数组:

    a := [...]int{0, 1, 2, 3, 4, 5}
    reverse(a[:])
    fmt.Println(a) // "[5 4 3 2 1 0]"
    

    假设要将slice中前n个元素和后面全部的元素调换位置的话(以第n个元素为支点,向左旋转),当中一个办法是调用三次reverse函数,第一次是反转前n个元素。然后是反转剩下全部元素,最后是反转整个slice(假设是向右旋转,则将第一个和第三个函数对调位置就可以)。

    s := []int{0, 1, 2, 3, 4, 5}
    // 以2为支点,向左旋转
    reverse(s[:2])
    reverse(s[2:])
    reverse(s)
    fmt.Println(s) // "[2 3 4 5 0 1]"
    

    要注意上面代码中。slice和数组的初始化语法的差异。尽管它们的语法非常相似。都是用花括号包括的元素序列,可是slice是不须要指明长度的,因此会隐式的创建一个底层数组,然后slice内的指针会指向这个数组。初始化slice和初始化数组一样,能够使用值序列或者使用索引-值列表(见3.1节)。

    在3.1节中,我们提到了若数组的类型能够比較。那数组能够比較,可是slice之间是不能通过==操作符进行比較的!标准库中提供了高度优化的bytes.Equal函数,能够用来推断两个[]byte是否相等,只是对于其他类型的slice,我们必须实现自己的比較函数:

    func equal(x, y []string) bool {
        if len(x) != len(y) {
            return false
        }
        for i := range x {
            if x[i] != y[i] {
                return false
            }
        }
        return true
    }
    

     为什么Go语言不支持slice的比較运算呢?第一个原因。slice是引用类型,一个slice甚至能够引用自身。尽管有非常多解决的方法,可是没有一个是简单有效的。第二个原因,由于slice是间接引用,因此一个slice在不同一时候间可能包括不同的元素-底层数组的元素可能被改动。

    仅仅要一个数据类型能够做相等比較,那么就能够用来做map的key,map这样的数据结构对key的要求是:假设最開始时key是相等的,那在map的生命周期内,key要一直相等,因此这里key是不可变的。而对于指针或chan这类引用类型,==能够推断两个指针是否引用了想同的对象,是实用的,可是slice的相等測试充满了不确定性,因此。安全的做法是禁止slice之间的比較操作。

    唯一例外:slice能够和nil进行比較。比如。

    if summer == nil { /* ... */ }
    

    slice是引用类型,因此它的零值是nil。

    一个nil slice是没有底层数组的,长度和容量都是0,可是也有非nil的slice,长度和容量也是0,比如[]int{}或make([]int, 3)[3:]。

    我们能够通过[]int(nil)这样的类型转换生成一个[]int类型的nil值。

    var s []int    // len(s) == 0, s == nil
    s = nil        // len(s) == 0, s == nil
    s = []int(nil) // len(s) == 0, s == nil
    s = []int{}    // len(s) == 0, s != nil
    

    由上可知。假设要測试一个slice是否为空。要使用len(s) == 0。除了能够和nil做相等比較外,nil slice的使用和0长度slice的使用方式同样:比如,前文的函数reverse(nil)就是安全的。

    除非包文档特别说明。否则全部的Go函数都应该以同样的方式对待nil slice和0长度slice(byte包中的部分函数会对nil值slice做特殊处理)。

    内置函数make能够用于创建一个指定类型、长度、容量的slice。非常多时候,容量參数能够省略。这样的情况下,容量等于长度:

    make([]T, len)
    make([]T, len, cap) // same as make([]T, cap)[:len]
    

    实际上,make会创建一个匿名的底层数组,然后返回一个slice值。仅仅有通过该值才干引用匿名的底层数组。在上面第一条语句中。slice的范围和底层数组范围一致。在第二条语句中,slice引用了底层数组前len个元素,可是slice的容量和底层数组的长度一致。因此slice能够在len不够用时,自己主动增长,仅仅要长度不超过cap就可以。


    4.2.1. append函数

    内置函数append用于向slice的末端追加元素:

    var runes []rune
    for _, r := range "Hello, 世界" {
        runes = append(runes, r)
    }
    fmt.Printf("%q
    ", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
    

    上面的循环使用append构建了包括9个rune的slice(这个问题能够直接通过[]rune("Hello,世界")来解决)。

    理解append对于理解slice的底层原理是非常有帮助的,以下是appendInt 1.0版本号,用于处理[]int类型的slice:

    func appendInt(x []int, y int) []int {
        var z []int
        zlen := len(x) + 1
        if zlen <= cap(x) {
            // 还有空间,扩展slice
            z = x[:zlen]
        } else {
            // 空间不足,分配新的数组
            // 新数组的长度按2的倍数增长.
            zcap := zlen
            if zcap < 2*len(x) {
                zcap = 2 * len(x)
            }
            z = make([]int, zlen, zcap)
            copy(z, x) // a built-in function; see text
        }
        z[len(x)] = y
        return z
    }
    

    每次调用appendInt,先检測slice的底层数组容量是否足够。假设足够,直接扩展slice(仍然在底层数组中)。然后将y元素复制过去。这时x和z共享底层数组。

    假设没有足够的容量,appendInt会又一次分配一个足够大的数组。然后将x全部复制过去。再在尾部加入y。

    这时,x和z引用的是不同的底层数组。

    上面那种通过循环来一个一个复制元素尽管非常直接非常easy,可是内置函数copy更适合这样的场景。

    copy能够将一个slice复制给另一个同类型的slice,copy第一个參数是目标slice,第二个參数是源slice,能够通过这样的方式来记住參数顺序: dst = src,将src'赋给'dst。dst和src两个slice能够共享底层数组,甚至重叠。copy将返回复制的元素个数-等于两个slice中较小的长度值。所以不用操心越界问题。

    为了降低内存分配次数、提升利用率。新分配的数组的长度要大于x + y的长度,这里有个简单的办法。每次扩展数组时将长度翻倍。能够降低了多次内存分配,也保证了加入元素是常数时间操作:

    func main() {
        var x, y []int
        for i := 0; i < 10; i++ {
            y = appendInt(x, i)
            fmt.Printf("%d cap=%d	%v
    ", i, cap(y), y)
            x = y
        }
    }
    

    容量的每次变化都会导致内存分配和内存拷贝,由于须要创建新的底层数组。并拷贝元素过去:

    0  cap=1    [0]
    1  cap=2    [0 1]
    2  cap=4    [0 1 2]
    3  cap=4    [0 1 2 3]
    4  cap=8    [0 1 2 3 4]
    5  cap=8    [0 1 2 3 4 5]
    6  cap=8    [0 1 2 3 4 5 6]
    7  cap=8    [0 1 2 3 4 5 6 7]
    8  cap=16   [0 1 2 3 4 5 6 7 8]
    9  cap=16   [0 1 2 3 4 5 6 7 8 9]
    

    先看一下i=3那一次循环,循环開始前x包括了[0 1 2]三个元素,容量是4,这时底层数组末尾另一个位置能够将新元素拷贝进来,不须要内存分配,循环后。y的长度和容量都是4而且x和y引用同样的底层数组:



    再来看i=4这次循环。循环開始前,x的底层数组没有新空间了,因此appendInt又一次创建一个容量为8的底层数组。将x的全部元素都复制过去,然后在末尾加入新元素4。

    循环后,y的长度是5,容量是8,因此还有3个空暇位置。后面的三次循环都不须要又一次分配底层数组。

    在i=4这次循环中,y和x引用了不同的底层数组:

    内置函数append使用了比appendInt更复杂的扩展策略。因此我们无法得知append调用是否导致了新的内存分配。也不能确定新的slice和旧的slice是否引用同样的底层数组,同一时候我们也不能确定在旧的slice上操作是否会影响新的slice。

    因此一般这样使用append:

    runes = append(runes, r)
    

    将值直接赋给旧的slice,这样的更新slice变量的写法在调用append时是必要的。在实际应用中,除了append。其他不论什么可能导致长度、容量或底层数组变化的操作,都须要更新旧的slice变量。尽管slice是引用类型。訪问底层数组是间接訪问的,可是slice本身就是一个结构体,是一个值类型。里面包括了指针、长度、容量字段,因此要更新slice就要像上面那样有显式的赋值操作。从这个角度来说。slice并非一个纯粹的引用类型:

    type IntSlice struct {
        ptr      *int
        len, cap int
    }
    

    我们的appendInt函数每次仅仅能加入一个元素,而append函数能够加入多个,甚至是一个slice:

    var x []int
    x = append(x, 1)
    x = append(x, 2, 3)
    x = append(x, 4, 5, 6)
    x = append(x, x...) // 追加slice x
    fmt.Println(x)      // "[1 2 3 4 5 6 1 2 3 4 5 6]"
    

    如今我们将appendInt进行完好,以达到和append函数相似的效果,当中用到了变參函数,在下一章中会详解:

    func appendInt(x []int, y ...int) []int {
        var z []int
        zlen := len(x) + len(y)
        // ...expand z to at least zlen...
        copy(z[len(x):], y)
        return z
    }


    4.2.2. 一些就地操作的技巧

    再来看看很多其他的slice就地操作的样例,比如旋转、反转、改动元素。给定一个字符串列表,nonempty将在原有slice空间内进行操作,然后返回不包括空字符串的列表:

    // Nonempty is an example of an in-place slice algorithm.
    package main
    
    import "fmt"
    
    // nonempty returns a slice holding only the non-empty strings.
    // The underlying array is modified during the call.
    func nonempty(strings []string) []string {
        i := 0
        for _, s := range strings {
            if s != "" {
                strings[i] = s
                i++
            }
        }
        return strings[:i]
    }
    

    这里比較精妙的地方是,输入slice和输出slice共享底层数组。这样就避免了又一次分配一个数组。只是造成的问题是原来的数据可能会被覆盖:

    data := []string{"one", "", "three"}
    fmt.Printf("%q
    ", nonempty(data)) // `["one" "three"]`
    fmt.Printf("%q
    ", data)           // `["one" "three" "three"]`
    

    依据4.2.1的内容,我们一般会这样使用nonempty函数:

      data = nonempty(data)

    nonempty函数也能够使用append实现:

    func nonempty2(strings []string) []string {
        out := strings[:0] // zero-length slice of original
        for _, s := range strings {
            if s != "" {
                out = append(out, s)
            }
        }
        return out
    }
    

    不管採用哪种实现方式。像这样复用数组空间要求每一个输入值最多仅仅有一个输出值,这样的模式对非常多算法都是适用的:过滤一个值序列或者合并值序列中的相邻元素。相似这样的使用方法都是较为复杂的,也是较为少见的。可是在某些场合中能够发挥奇效。

    能够用slice来模拟栈(stack)。给定一个空的slice。相应空的stack,然后使用append函数将新值入栈:

    stack = append(stack, v) // push v
    

    栈顶相应的是slice最后一个元素:

    top := stack[len(stack)-1] // top of stack
    

    通过切片操作能够弹出栈顶元素:

    stack = stack[:len(stack)-1] // pop
    

    要删除slice某个元素i并保存原有的元素顺序,能够通过copy将i后面的元素依次向前移动一位:

    func remove(slice []int, i int) []int {
        copy(slice[i:], slice[i+1:])
        return slice[:len(slice)-1]
    }
    
    func main() {
        s := []int{5, 6, 7, 8, 9}
        fmt.Println(remove(s, 2)) // "[5 6 8 9]"
    }
    

    假设删除元素且不用保持原有顺序。能够用最后一个元素覆盖被删除的元素:

    func remove(slice []int, i int) []int {
        slice[i] = slice[len(slice)-1]
        return slice[:len(slice)-1]
    }
    
    func main() {
        s := []int{5, 6, 7, 8, 9}
        fmt.Println(remove(s, 2)) // "[5 6 9 8]
    }
    


    练习 4.3: 重写reverse函数,使用数组指针取代slice。

    练习 4.4: 编写一个rotate函数,通过一次循环完毕旋转。

    练习 4.5: 写一个函数,就地消除[]string中相邻的反复字符串

    练习 4.6: 编写一个函数,给定一个UTF-8编码的[]byte类型的slice,就地将该slice中的相邻的两个Unicode空格(參见unicode.IsSpace)替换成一个ASCII空格

    练习 4.7: 改动reverse函数,给定一个[]byte,相应的是UTF-8编码的字符串,然后就地反转。能否够做到无内存分配?



    文章全部权:Golang隐修会 联系人:孙飞。CTO@188.com!


  • 相关阅读:
    Flask 随记
    Notes on Sublime and Cmder
    Algorithms: Design and Analysis Note
    LeetCode 215 : Kth Largest Element in an Array
    LeetCode 229 : Majority Element II
    LeetCode 169 : Majority Element
    LeetCode 2:Add Two Numbers
    LeetCode 1:Two Sum
    Process and Kernel
    安装好scala后出现“找不到或无法加载主类”的问题
  • 原文地址:https://www.cnblogs.com/yutingliuyl/p/7149504.html
Copyright © 2011-2022 走看看