跳至主要內容

数组和切片

Mr.Liu大约 8 分钟Go

数组和切片

数组

数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),这种类型可以是意的原始类型,比如int、string等,也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。

在Go中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说[5]int和[10]int是两个不同的类型。

数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。

数组定义

一维数组

var 数组变量名 [元素数量] T

示例

// 数组的长度是类型的一部分
var arr1 [3]int
var arr2 [4]string
fmt.Printf("%T, %T \n", arr1, arr2)

// 数组的初始化 第一种方法
var arr3 [3]int
arr3[0] = 1
arr3[1] = 2
arr3[2] = 3
fmt.Println(arr3)

// 第二种初始化数组的方法
var arr4 = [4]int {10, 20, 30, 40}
fmt.Println(arr4)

// 第三种数组初始化方法,自动推断数组长度
var arr5 = [...]int{1, 2}
fmt.Println(arr5)

// 第四种初始化数组的方法,指定下标
a := [...]int{1:1, 3:5}
fmt.Println(a)

二维数组

Go语言支持多维数组,我们这里以二维数组为例(数组中又嵌套数组):

var 数组变量名 [元素数量][元素数量] T

示例

// 二维数组
var array5 = [2][2]int{{1,2},{2,3}}
fmt.Println(array5)

另外我们在进行数组的创建的时候,还可以使用类型推导,但是只能使用一个 ...

// 二维数组(正确写法)
var array5 = [...][2]int{{1,2},{2,3}}

错误写法

// 二维数组
var array5 = [2][...]int{{1,2},{2,3}}

数组操作

在 Go 语言中,数组类型支持一些基本的操作,如遍历、修改、拷贝等。

遍历数组

可以使用for​循环遍历数组,也可以使用range​关键字来遍历数组。

a := [3]int{1, 2, 3}

// 使用for循环遍历数组
for i := 0; i < len(a); i++ {
    fmt.Println(a[i])
}
// Output:
// 1
// 2
// 3

// 使用range关键字遍历数组
for i, v := range a {
    fmt.Println(i, v)
}
// Output:
// 0 1
// 1 2
// 2 3

修改数组

要修改数组中的元素,只需使用下标运算符([]​)来指定要修改的元素的位置,并将新值赋给它即可。

a := [3]int{1, 2, 3}
a[1] = 4
fmt.Println(a) // Output: [1 4 3]

数组是值类型,赋值和传参会赋值整个数组,因此改变副本的值,不会改变本身的值

var array1 = [...]int {1, 2, 3}
array2 := array1
array2[0] = 3
fmt.Println(array1, array2) // Output: [1 2 3] [3 2 3]

内置函数

  • len(array)​:返回数组的长度
  • cap(array)​:返回数组的容量,对于数组类型,容量和长度是相等的

以下是一些使用数组内置函数的示例:

a := [3]int{1, 2, 3}
fmt.Println(len(a)) // Output: 3
fmt.Println(cap(a)) // Output: 3

切片

切片(Slice)是一个动态数组,是对数组的封装。与数组相比,切片具有更高的灵活性和便利性,可以动态地增加或减少其容量和长度。

切片本身并不存储数据,它只是一个对底层数组的引用。它的内部结构包含地址长度容量

切片定义

直接定义

在Go语言中,切片类型的定义格式为:[]T​,其中,T​表示切片中元素的类型。

以下是一些初始化和定义切片的示例:

// 声明切片,把长度去除就是切片
// 定义一个字符串切片,并初始化2个元素
a := []string{"hello", "world"}

// 定义一字符串切片,但不初始化
var c []string

// 声明一个整数切片,但不初始化
var d []int

基于数组定义切片

由于切片的底层就是一个数组,所以我们可以基于数组来定义切片

// 基于数组定义切片
a := [5]int {55,56,57,58,59}
// 获取数组所有值,返回的是一个切片
b := a[:]  // Out: [55 56 57 58 59]
// 从数组获取指定的切片
c := a[1:4]  // Out: [56 57 58]
// 获取 下标3之前的数据(不包括3)
d := a[:3]  // Out: [55 56 57]
// 获取下标3以后的数据(包括3)
e := a[3:]  // Out: [58 59]

同理,我们不仅可以对数组进行切片,还可以切片在切片

切片操作

遍历切片

切片的遍历和数组是一样的

var slice = []int{1,2,3}
for i := 0; i < len(slice); i++ {
    fmt.Print(slice[i], " ")
}

修改切片

要修改数组中的元素,只需使用下标运算符([]​)来指定要修改的元素的位置,并将新值赋给它即可。

a := []int{1, 2, 3}
a[1] = 4
fmt.Println(a) // Output: [1 4 3]

切片是引用类型,赋值和传参会实际都是对同一个数组的引用,因此改变副本的值,也会改变本身的值

package main

import "fmt"

func main() {
    var array1 = []int {1, 2, 3}
    array2 := array1
    array2[0] = 3
    fmt.Println(array1, array2) // Output: [3 2 3] [3 2 3]
}

内置函数

  • len(array)​:返回切片的长度
  • cap(array)​:返回切片的容量,对于数组类型,容量和长度是相等的
  • copy(destSlice, srcSlice []T) int​:将源切片中的元素复制到目标切片中,返回实际复制的元素个数,数组是不支持该操作的
  • make ([]T, size, cap)​:动态创建一个指定长度和容量的切片
  • append(srcSlice []T, num1, num2, ...)​:可以为切片动态添加元素,返回一个新切片,在为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的容量会发生改变

以下是一些使用切片内置函数的示例:

package main

import "fmt"

func main() {
    var a = []int{1, 2, 3, 4}
    fmt.Println(len(a)) // Output: 3
    fmt.Println(cap(a)) // Output: 3

    var b = make([]int, 7, 10)
    fmt.Println(len(b)) // Output: 7
    fmt.Println(cap(b)) // Output: 10

    copy(b, a)
    fmt.Println(b) // Output: [1 2 3 4 0 0 0]

    b = append(b, 1)            // 追加1个元素
    b = append(b, 1, 2, 3)      // 追加多个元素, 手写解包方式
    b = append(b[:1], b[2:]...) // 追加一个切片, 切片需要解包, 切片本身不支持删除操作,但是可以模拟实现

    fmt.Println(b)      // Output: [1 3 4 0 0 0 1 1 2 3]
    fmt.Println(len(b)) // Output: 10
    fmt.Println(cap(b)) // Output: 20
}

排序操作

冒泡升序排序

package main

import "fmt"

func main() {
    var numSlice = []int{9, 8, 7, 6, 5, 4}
    for i := 0; i < len(numSlice); i++ {
        flag := false
        for j := 0; j < len(numSlice)-i-1; j++ {
            if numSlice[j] > numSlice[j+1] {
                var temp = numSlice[j+1]
                numSlice[j+1] = numSlice[j]
                numSlice[j] = temp
                flag = true
            }
        }
        if !flag {
            break
        }
    }
    fmt.Println(numSlice)
}

选择升序排序

package main

import "fmt"

func main() {
    // 编写选择排序
    var numSlice2 = []int{9, 8, 7, 6, 5, 4}
    for i := 0; i < len(numSlice2); i++ {
        for j := i + 1; j < len(numSlice2); j++ {
            if numSlice2[i] > numSlice2[j] {
                var temp = numSlice2[i]
                numSlice2[i] = numSlice2[j]
                numSlice2[j] = temp
            }
        }
    }
    fmt.Println(numSlice2)
}

切片扩容

当切片的长度不足以容纳新的元素时,Go 语言会自动扩容切片的容量

当创建多个切片时,如果它们底层引用的是同一个数组,则它们之间会共享底层数组。以下是一个例子:

// 创建一个长度为 5,容量为 10 的切片
numbers := make([]int, 5, 10)

// 创建两个切片,它们共享同一个底层数组
slice1 := numbers[1:3]
slice2 := numbers[2:5]

// 修改切片中的元素
slice1[0] = 10
slice2[1] = 20

// 遍历切片中的所有元素,发现它们都已经被修改了
for _, v := range numbers {
    fmt.Println(v)
}

在上面的例子中,我们创建了一个长度为 5,容量为 10 的切片 numbers​,然后创建了两个切片 slice1​ 和 slice2​,它们都共享同一个底层数组 numbers​。然后我们修改了切片 slice1​ 和 slice2​ 中的元素,最后遍历切片 numbers​ 中的所有元素,发现它们都已经被修改了。

需要注意的是,如果修改了其中一个切片的长度或容量,那么它们之间就不再共享同一个底层数组了。例如,如果我们将 slice1​ 的长度扩展到 3,那么它就不再共享底层数组了:

slice1 = append(slice1, 30)

// 修改 slice1 后,它就不再共享底层数组了
for _, v := range numbers {
    fmt.Println(v)
}

当一个切片的长度超过了其容量时,底层数组就会重新分配内存,新的底层数组会有足够的容量来容纳新的元素,然后将原来的元素复制到新的底层数组中,并将切片指向新的底层数组。因此,无论是使用数组还是使用长度为0的切片作为底层数组,在进行切片扩容操作时,都有可能失去与其他切片共享底层数组的效果。所以在使用多个切片共享底层数组时,需要特别注意切片的容量和扩容操作