Golang教程第7篇-数组、切片

1.数组

1.1.数组特点

数组是一同种数据类型的固定长度的序列

定义方式为var a [10]int 表示有5个元素的数组,a [5]int 与a [10]int是不同类型的数组,因为长度也是数组类型的部分

下标与其他语言一样,从0开始,以数组长度-1结尾

在使用数组时要注意以下几点:
1、下标值不要超过范围,否则会panic
2、可以使用for一般语句来遍历数组的每一个元素,也能用过for range语句
3、数组时数值类型,副本值不会影响原始值

下面用一个例子来简要说明以上三点

package main

import (
    "fmt"
)

func main() {
    var a [10]int  
    i := 10
    a[i] = 100
    fmt.Println(a)
}

编译执行
05go72
图中可以看到索引不在范围内panic了

package main

import (
    "fmt"
)

func main() {
    var a [10]int
    fmt.Println("for普通循环")
    for i := 0;i < len(a);i++ {
        fmt.Printf("a[%d]=%d\t",i,a[i])
    }
    
    fmt.Println()
    
    fmt.Println("for range")
    for i,v := range a {
        fmt.Printf("a[%d]=%d\t",i,v)
    }
}

编译执行
05go73
上面用了两种方式来遍历数组

package main

import (
    "fmt"
)

func Test(a [10]int) {
    a[0] = 100
}

func Test1(a *[10]int) {
    (*a)[0] = 100 
}

func main() {
    var a [10]int
    fmt.Println(a)
    
    Test(a)
    fmt.Println(a)
    
    Test1(&a)
    fmt.Println(a)
}

编译执行

05go74
数组是数值类型,调用函数的处理只是传递了一个变量副本,不会影响原变量的值

数组在进行计算的时候很方便迅速,比如前面计算斐波拉契数列,打印前100个数
前面使用的是递归的方式实现

package main 

import (
    "fmt"
)

func Process(a int) {
    for i := 0;i < a;i++ {
        result := Test(i)
        fmt.Printf("%d\t",result)
    }
}

func Test(a int) int {
    if a <= 1 {
        return 1
    }
    return Test(a - 1) + Test(a - 2) 
}

func main() {
    Process(10)
}

编译执行
05go75

这种方式实现在传递较大数值的时候会很慢,如果用数组来实现就会快很多

package main

import (
    "fmt"
)

func Test(a [10]int) {
    intlen := len(a)
    for i := 0;i < intlen;i++ {
        if i <= 1 {
            a[i] = 1 
        } else {
            a[i] = a[i-1] + a[i-2]
        }
        fmt.Printf("%d\t",a[i])
    }
}

func main() {
    var a [10]int
    Test(a)
}

编译执行
05go76

上面直接传递的一个数组进去,还可以换一种写法

package main

import (
    "fmt
)

func Test(n int) {
    a := make([]int,n)
    for i := 0;i < len(a);i++ {
        if i <= 1 {
            a[i] = 1
        } else {
            a[i] = a[i-2] + a[i-1]
        }
    }
    for _,v := range a {
        fmt.Println(v)
    }
}

func main() {
   Test(10)
}

上面这种写法使用make创建了一个slice,不能使用下面的方式来创建一个数组

a := make([n]int)   

因为数组必须是定长,不能是变长n

1.2.数组初始化

var a [5]int = [5]int{1,2,3,4,5}
var a [5]int = [5]int{1,2,3} // 后面两元素为0
var a [6]int = [...]int{1,2,3,4,5,6}
指定下标初始化
var str [5]string = [5]string{3:"hello",4:"world"}

package main

import (
    "fmt"
)

func main() {
    var a1 [5]int = [5]int{1,2,3,4,5}  
    var a2 = [5]int{1,2,3,4,5}   
    var a3 = [...]int{1,2,3,4,5}  
    var a4 = [5]int{1,2,3} 
    var a5 = [...]int{1:100,3:200}
    var a6 = [5]int{1:100,3:200}
    var str2 = [5]string{1:"hello",3:"world"}
    var str3 = [...]string{1:"hello",3:"world"}
    
    fmt.Println(a1)
    fmt.Println(a2)
    fmt.Println(a3)
    fmt.Println(a4)
    fmt.Println(a5)
    fmt.Println(a6)
    fmt.Println(str2)
    fmt.Println(str3)
}

编译执行
05go77

1.3.二维数组

初始化二维数组
var a [2][5]int = [...][5]int{{1,2,3,4,5},{6,7,8,9,10}}
遍历二维数组

package main

func main() {
    var a [2][5]int = [...][5]int{{1,2,3,4,5},{6,7,8,9,10}}   
    for row,v := range a {
        for col,v1 := range v {
            fmt.Printf("(%d %d)=%d ",row,col,v1)
        }
        fmt.Println()
    }
}

编译执行
05go78

2.切片

2.1.初始化切片

切片是数组的引用,底层是数组,一般有两种方式初始化切片,数组方式和make,先来看看数组的方式

package main

import (
    "fmt"
)

func TestSlice() {
    var slice []int
    var arr [5]int = [...]int{1,2,3,4,5}
    
    slice = arr[2:5]
    fmt.Println(slice)
    fmt.Println(len(slice))
    fmt.Println(cap(slice))
    
    slice = arr[2:4]
    fmt.Println(slice)
    fmt.Println(len(slice))
    fmt.Println(cap(slice))
    
    slice = slice[0:1]
    fmt.Println(slice)
    fmt.Println(len(slice))
    fmt.Println(cap(slice))
}

func main() {
    TestSlice()
}

编译执行
05go79
上面的例子使用数组的方式对切片进行初始化,同时利用内置函数len和cap分别计算切片的长度和容量,要注意容量是从切片下标开始的地方到数组结尾这段来计算的,同时切片还可以再切片

slice在初始化的时候有一些简写方式

package main

import (
    "fmt"
)

func main() {
    vat slice []int
    var arr [5]int = [...]int{1,2,3,4,5}
    
    //取整个数组
    slice = arr[:]
    fmt.Println(slice)
    
    //取下表标1~后面所有的元素
    slice = arr[1:]
    fmt.Println(slice)
    
    //取下表0~2所有元素
    slice = arr[:3]
    fmt.Println(slice)
    
    //切片包含最后一个元素
    slice = arr[:len(slice)]
    fmt.Println(slice)
    
    //切片去掉最后一个元素
    slice = arr[:len(slice)-1]
    fmt.Println(slice)
}

slice是引用类型,其底层结构包含了指向数组的指针,切片长度和切片容量,下面通过改变一个切片的元素值来输出前后的切片来说明切片是一个引用类型

package main

import (
    "fmt"
)

type slice struct {
    ptr *[100]int
    cap int
    len int
}

func Make1(s slice,cap int) slice {
    s.ptr = new([100]int)
    s.cap = cap
    s.len = 0
    return
}
func Modify(s slice) {
    s.ptr[1] = 1000
}

func TestSlice() {
    var s1 slice
    s1 = Make1(s1,10)
    s.ptr[0] = 100
    Modify(s1)
    fmt.Println(s1)
}

func main() {
    TestSlice()
}

编译执行
05go80

这个例子是自定义了Go里面的make函数来对一个自定义的slice类型进行初始化,slice类型通过type关键字来定义,里面包含了slice底层的三个字段,指针、容量、长度,Make1函数是模仿make自定义的一个专门在本例中对slice类进行初始化的函数,然后通过函数Modify来传递slice进行修改,最终输出的结果是slice引用的数组被改变,而这里有没有使用&来传递,所以这里自定义的slice也是是引用类型,这里的关键就是切片结构有一个指针是指向一个数组

下面传递一个切片看看效果

package main

import (
    "fmt"
)

func TestSlice() {
    var a []int = []int{1,2,3,4,5}
    Modify(a)
    fmt.Println(a)
}
func Modify(b []int) {
    b[0] = 10
}

func main() {
    TestSlice()
}

05go81

切片其实是一个地址,其底层的数据结构是一个指针,切片底层的地址是指向引用数组的第一个元素的地址

package main

import (
    "fmt"
)

func TestSlice() {
    var a [5]int = [...]int{1,2,3,4,5}
    slice = a[1:]
    fmt.Printf("%p\n",&a[1])
    fmt.Printf("%p\n",slice)
}

func main() {
    TestSlice()
}

编译执行
05go82
输出结果一样,这就表明slice指向数组的第一个元素的地址(注意这里之所以是第一个是因为slice是从下标1开始的,如果下表是2那么指向的就是数组元素a[2]的地址了)

2.2.append操作切片

切片可以通过append内置函数来添加元素或者添加slice


 package main 
 
 import (
     "fmt"
 )
 
 func TestSlice() {
     var a int[5] = [...]int{1,2,3,4,5}
     s := a[1:]
     fmt.Printf("s=%p a[1]=%p\n",s,&a[1])
     s = append(s,10)
     s = append(s,10)
     s = append(s,10)
     s = append(s,10)
     s = append(s,10)
 }
 
 func main() {
     TestSlice()
 }

编译执行
05go83
从上面的结果看出输出了一个9个元素的切片,这就有个问题,前面学过切片是指向一个数组的,而数组的长度是固定的,这里添加元素后的切片操过了底层数组提供的最大容量,那么底层是如何实现这个过程的?

这是因为添加后超过容量了,这时会开辟另外一块内存放置该slice,这时候slice底层指向的数组就不是原来的了,可以通过打印appen添加元素前后slice的指向地址看看两者是否一样,不一样说明的确是另外开了内存来存放新的slice指向的数组

看下面这个例子通过比较append元素后slice指向的地址

package main

import (
    "fmt"
)

func TestSlice() {
    var a [5]int = [...]int{1,2,3,4,5}
    s := a[1:]
    fmt.Printf("s=%p a[1]=%p\n",s,&a[1])
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    fmt.Println(s)
    fmt.Print("s=%p a[1]=%p\n",s,&a[1]")
}

func main() {
    TestSlice()
}

编译执行
05go84
从输出结果可以看到地址发生了改变,其实还可以通过修改s中的某个元素,然后输出数组a,通过对比append前后数组a的结果,如果没有改变说明append元素超过容量后的slice指向的地址发生了改变,看下面的例子

package main

import (
    "fmt"
)

func TestSlice() {
    var a [5]int = [...]int{1,2,3,4,5}
    s := a[1:]
    s[1] = 100
    fmt.Println(a)
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
    s[1] = 1000
    fmt.Println(a)
}

func main() {
    TestSlice()
}

编译执行
05go85
从输出结果看数组a的值在append后并没有改变,这说明扩容后的slice不再指向数组a

那么slice的扩容机制是如何的呢?下面通过一个例子来说明

package main 

import (
    "fmt"
)

func TestSlice() {
    var a [5]int = [...]int{1,2,3,4,5}
    s := a[1:]
    fmt.Printf("len[%d] cap[%d]\n",len(s),cap(s))
    s = append(s,10)
    s = append(s,10)
    fmt.Printf("len[%d] cap[%d]\n",len(s),cap(s))
    s = append(s,10)
    s = append(s,10)
    s = append(s,10)
}

func main() {
    TestSlice()
}

编译执行
05go86
从结果输出可以看到扩容是翻倍扩容的

append还能在切片上追加切片

package main

func TestSlice() {
    var a [5]int = [...]int{1,2,3,4,5}
    s := a[1:]
    fmt.Println(s)
    t := a[2:]
    fmt.Println(t)
    m := appedn(s,t...)
    fmt.Println(m)
}

func main() {
    TestSlice()
}

编译执行
05go87

2.3.for range切片

package main

import (
    "fmt"
)

func TestSlice() {
   var a []int = []int{2,3,4}
   for i,v := range a {
       fmt.Printf("index[%d] value[%d]\n",i,v)
   }
}

func main() {
    TestSlice()
}

编译执行
05go88

2.4.切片拷贝

package main

import (
    "fmt"
)

func TestSlice() {
    var a []int = []int{1,2,3}
    s := make([]int,5)
    m = copy(s,a)
    fmt.Println(m)
    fmt.Println(s)
}
func main() {
    TestSlice()
}

编译执行
05go89
上面将切片a的元素拷贝到切片s中,拷贝的元素个数为3

2.5.slice与string

string的底层就是一个byte数组,可以类似slice直接取切片,但是与数组不同,string是不可变的

package main

import (
    "fmt"
)

func TestSlice() {
    str := "hello world"
    str1 := str[0:5]
    str2 := str[6:]
    fmt.Println(str1)
    fmt.Println(str2)
}

func main() {
    TestSlice()
}

05go90
既然string是不可变的,那么该如何去修改字符串中的字符呢?
先看一种写法

package main

import (
    "fmt"
)

func TestSlice() {
   str := "hello world"
   str[0] = 'a'
}
func main() {
    TestSlice()
}

这种写法会报错,str[0]这种写法无法编译,如果要要修改字符串的内容,可以先将字符串改成切片,然后修改,修改后再转成字符串

package main

import (
    "fmt"
)

func TestSlice() {
    str := "中hello world"
    str1 := []rune(str)
    str1[0] = 'r'
    str = string(str1)
    fmt.Printf("%s",str)
}
func main() {
    TestSlice()
}

编译执行
05go91

2.6.排序查找

一般分为字符串排序、数值排序、浮点数排序,需要调用包sort里的函数,查找需要先排序再查找,下面通过一个例子来表明各自的用法

package main

import (
    "fmt"
    "sort"
)

func TestSortInt() {
    var a = [...]int{10,9,8,7,6}
    // 整数排序
    sort.Ints(a[:])
    // 整数查找
    index := sort.SearchInts(a[:],10)
    fmt.Println(a)
    fmt.Println(index)
}

func TestSortStr() {
    var a = [...]string{"dd","aa","jj","vc"}
    // 字符串排序
    sort.Strings(a[:])
    // 字符串查找
    index := sort.SearchStrings(a[:],"jj")
    fmt.Println(a)
    fmt.Println(index)
}

func TestSortFloat() {
    var a = [...]float64{1.9,0.9,0.98,2.2}
    // 浮点数排序
    sort.Float64s(a[:])
    // 浮点数查找
    index := sort.SearchFloat64s(a[:],2.2)
    fmt.Println(a)
    fmt.Println(index)
}

func main() {
    TestSortInt()
    TestSortStr()
    TestSortFloat()
}

编译执行
06go92

在进行排序和查找必须使用最大容量长度的切片a[:],使用数组本身无法编译报错,这是因为排序是改变数组元素值,只有通过引用类型来修改才可以

3.包链接

sort