名人说:莫愁千里路,自有到来风。 ——钱珝
创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)目录
- 1. 数组
- ① 什么是数组
- ② 数组的声明
- ③ 初始化数组的几种方式
- ④ 遍历数组元素
- ⑤ 数组为值类型
- ⑥ 数组的排序
- ⑦ 多维数组
- 2. 切片(动态数组的抽象)
- ① 定义切片
- ② make来创建切片
- ③ 切片扩容
- ④ 遍历切片
- ⑤ 扩容的内存分析
- 3、小结
Go语言中的数组和切片是管理集合数据的重要方式,具有不同的特性和用途。在这篇博客中,我们将深入探讨数组和切片的相关知识点,包括它们的定义、操作方法及其内部机制。
1. 数组
① 什么是数组
数组是同一种数据类型元素的集合。在Go语言中,数组是值类型,数组的长度在声明时就已经确定,且不能改变。关于数组可以这样理解,有一个收纳盒,专门用于收集同一类书的,但是这个收纳盒容量是有限的,自从这个收纳盒生产出来大小就已经确定了,而且不能再改变,而这个收纳同一类型的盒子也就是数组。
② 数组的声明
数组声明的基本语法格式为:
var arr [n]Type
其中n
表示数组的长度,Type
表示数组中元素的类型。
案例:数组的定义、赋值、打印
package main
import "fmt"
/*
数组:相同类型数据的集合
*/
// 数组
func main() {
//array 数组定义,变量
//数组也是一种数据类型
//数组的定义:[数组的大小size] 变量的类型
//意思为:定义一组该类型的数组集合,大小为size,最多可以保存size个数
var arr1 [5]int
//[0,0,0,0,0]
//给数组赋值,下标index从0开始
arr1[0] = 100
arr1[1] = 200
arr1[2] = 300
arr1[3] = 400
arr1[4] = 500
//打印数组
fmt.Println(arr1)
//取出数组中的某个元素
fmt.Println(arr1[1])
//数组中常用的方法 len()获取数组长度 cap()获取数组容量
fmt.Println("数组的长度:", len(arr1))
fmt.Println("数组的容量:", cap(arr1))
//修改数组的值,index 1 代表数组中的第二个数据
arr1[1] = 50
fmt.Println("修改后的数组:", arr1)
fmt.Println(arr1[1])
}
③ 初始化数组的几种方式
- 指定所有元素初始化
arr := [5]int{1, 2, 3, 4, 5}
- 部分元素初始化,未初始化元素为零值
arr := [5]int{1, 2} // 其余元素初始化为0
- 根据初始化值自动确定数组长度
arr := [...]int{1, 2, 3, 4, 5}
案例:数组初始化
package main
import "fmt"
// 数组的赋值初始化
func main() {
//定义时初始化
var arr1 = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr1)
//快速初始化 :=
arr2 := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr2)
//需要注意的点
//数据假如来自用户, 但不知道究竟有多少数据
//... 写法
//Go的编译器会根据数组的长度来给 ...赋值,自动推导长度
//注意点:数组不是无限长,也是固定大小,大小取决于数组元素的个数
var arr3 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(len(arr3))
fmt.Println(arr3)
//数组默认值,给其中的几个位置赋值
//可使用{index:值}的方法
var arr4 [10]int
arr4 = [10]int{1: 100, 5: 500}
fmt.Println(arr4) // [0 100 0 0 0 500 0 0 0 0]
}
④ 遍历数组元素
遍历数组元素可以使用for
循环或for range
循环。
arr := [...]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
fmt.Println(arr[i])
}
或者
for index, value := range arr {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
上面那种方式大家可能比较熟悉,C、C++等语言中都是类似的写法,但是下面这种方式如果没学过python等语言,可能了解会少一些,因此在个人在这里解释一下:
这段代码实现了什么?
for循环遍历arr
数组(或切片)。
这段代码该怎么理解?
可以这样看,每次迭代,for
循环会将当前元素的索引赋值给index
变量,将当前元素的值赋值给value
变量,然后执行循环体内的语句。而其中的range arr
就是负责生成索引和对应值的部分,比如在循环刚开始,数组中index=0,arr[index] = 1,那么所生成的index值 = 0,value值 = arr[index] = 1。
让我们逐步解析这段代码:
-
for index, value := range arr { ... }
for
关键字开始一个循环。index, value
是每次迭代时,range
表达式返回的两个值。index
是当前元素的索引,value
是当前元素的值。range arr
表示对arr
进行遍历。arr
可以是数组或切片。range
每次迭代都会返回当前元素的索引和值。{ ... }
中的代码是循环体,会对arr
中的每个元素执行一次。
-
fmt.Printf("Index: %d, Value: %d\n", index, value)
- 这行代码在循环体内,使用
fmt.Printf
函数打印当前元素的索引和值。%d
是格式化字符串,用于表示整数。\n
是换行符。 - 在每次迭代中,
index
和value
将被设置为当前遍历到的元素的索引和值,然后将它们打印出来。
- 这行代码在循环体内,使用
举个具体的例子,如果arr
是一个包含三个元素的切片[10, 20, 30]
,那么输出将会是:
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
因此,一言以蔽之,这段代码的作用就是遍历数组(或切片)arr,并打印出每个元素的索引和值。
⑤ 数组为值类型
数组在Go语言中是值类型,当数组作为函数参数传递时,传递的是数组的副本而不是其引用。
func modifyArray(arr [5]int) {
arr[0] = 10 // 这里修改的是数组的副本
}
案例:数组,值类型
package main
import "fmt"
// 数组是值类型:所有赋值后的对象修改值不影响原来的对象
func main() {
//数组 [size]type
arr1 := [4]int{1, 2, 3, 4}
arr2 := [5]string{"yueliusu", "zilihuakai"}
fmt.Printf("%T\n", arr1) //[4]int
fmt.Printf("%T\n", arr2) //[5]string
//数组的值传递和int等基本类型一致
arr3 := arr1
fmt.Println(arr1)
fmt.Println(arr3)
//修改arr3观察arr1是否会变化
arr3[0] = 12
fmt.Println(arr1)
fmt.Println(arr3) // 数组是值传递,拷贝一个新的内存空间
}
⑥ 数组的排序
可以自定义实现,也可以使用sort
包可以对数组进行排序。例如,对一个整型数组进行升序排序:
package main
import "fmt"
import "sort"
func main() {
arr := [5]int{3, 1, 4, 5, 2}
sort.Ints(arr[:]) // 将数组转换为切片
fmt.Println(arr)
}
案例:自己实现排序算法,冒泡排序
package main
import "fmt"
// 冒泡:每次筛选出一个最大或者最小的数.
/*
index 0 1 2 3 4
value 12 99 79 48 55
*/
// 冒泡排序逻辑,两两比较,大的往后移或者前移。 大
// 第一轮 : 12 79 48 55 99 // 5
// 第二轮 : 12 48 55 79 99 // 4
// 第三轮 : 12 48 55 79 99 // 3 //
// 第四轮 : 12 48 55 79 99 //
// 第五轮 : 12 48 55 79 99
// 代码实践
/*
// 两个数判断,如果一个数大,则交换位置,大放到后面
if arr[x] > arr[x+1] {
arr[x], arr[x+1] = arr[x+1],arr[x]
}
// 多轮判断,for, 循环次数 【数组大小】
*/
func main() {
arr := [...]int{12, 99, 79, 48, 55, 1, 110, 111, 23, 44, 21, 312, 123, 21, 312}
fmt.Println("初始数组:", arr)
//冒泡排序
//1、多少轮
for i := 1; i < len(arr); i++ {
//2、筛选出来最大数
for j := 0; j < len(arr)-i; j++ {
//降序排序,比较大小,改变升降序只需要改变符号即可
if arr[j] < arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j] //平行赋值,无需第三者临时变量进行交换
}
}
}
fmt.Println(arr)
}
⑦ 多维数组
多维数组可以通过嵌套数组来声明(俗称套娃)。
一维,线性
二维,表格,数组套数组
三维,立体,数组套数组套数组
多维,继续嵌套
var multiArr [2][3]int
multiArr[0] = [3]int{1, 2, 3}
multiArr[1] = [3]int{4, 5, 6}
案例:多维数组
package main
import (
"fmt"
)
func main() {
//多维数组
//定义一个二维数组
arr := [3][4]int{
{0, 1, 0, 0}, //arr[0]
{0, 0, 1, 0}, //arr[1]
{0, 2, 0, 0}, //arr[2]
}
//二维数组
fmt.Println(arr[0])
fmt.Println(arr[0][1])
fmt.Println("----------")
//遍历二维数组
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr); j++ {
fmt.Println(arr[i][j])
}
}
// for range
for i, v := range arr {
fmt.Println(i, v)
}
}
2. 切片(动态数组的抽象)
① 定义切片
切片是对数组的封装,它提供了一个可动态扩展的序列。切片不存储任何数据,它**只是对底层数组的引用。**那么它与数组相同吗?
显然,切片与数组是不同的,切片的长度是动态的,可以根据需要扩展或缩减。切片背后实际上是对数组的引用,切片是引用类型。
var slice []int
补充一下关于切片的基础概念:
- 底层数组:每个切片都指向一个底层的数组,切片通过指针引用数组的一部分元素。
- 长度(Length):切片的长度是它所包含的元素个数。可以通过内置的
len
函数获取。 - 容量(Capacity):切片的容量是从其起始元素到底层数组末尾元素的个数。可以通过内置的
cap
函数获取。
案例:切片的定义
package main
import "fmt"
// 定义切片
func main() {
arr := [4]int{1, 2, 3, 4} //数组,定长
fmt.Println(arr)
var s1 []int //切片,不定长,长度是可变的
fmt.Println(s1)
//切片空片段,初始的切片中,默认为nil
if s1 == nil {
fmt.Println("切片是空的")
}
s2 := []int{1, 2, 3, 4} //切片 不定长
fmt.Println(s2)
fmt.Printf("%T,%T\n", arr, s2) //[4]int,[]int
fmt.Println(s2[1])
}
② make来创建切片
通过make
函数创建切片,可以指定切片的长度和容量。
//例如,创建一个长度为5,容量为10的切片
slice := make([]int, 5, 10)
案例:make创建切片
package main
import "fmt"
func main() {
//make()
//make([]Type, length, capacity) //创建一个切片,长度,容量
s1 := make([]int, 5, 10)
fmt.Println(s1)
fmt.Println(len(s1), cap(s1))
//思考:容量为10,长度为5,可以存放6个数据吗?
s1[0] = 10
//s1[7] = 200 //runtime error: index out of range [7] with length 5
//切片的底层还是数组 [0 0 0 0 0] [2000]
//直接去赋值是不行的,不要用常规的惯性想法来考虑,不加思索说出答案,思考的过程很重要。
fmt.Println(s1)
}
③ 切片扩容
当向切片追加元素超过其容量时,Go会自动进行扩容,一般会翻倍增加,比如说原本容量为2,你要放3个,放不下了,它会自动扩容为4,依次类推,扩容为8、16等
slice := make([]int, 0, 2)
for i := 0; i < 10; i++ {
slice = append(slice, i)
}
案例:切片扩容,自动翻倍
package main
import "fmt"
func main() {
//这段代码意味着创建了一个整型切片,初始长度为0,容量为5
s1 := make([]int, 0, 5)
fmt.Println(s1)
//切片扩容,append()
s1 = append(s1, 1, 2)
fmt.Println(s1)
//问题:容量只有5个,那能放超过5个的吗?当然,切片是会自动扩容的
s1 = append(s1, 2, 3, 4, 5, 6, 7, 8, 9)
fmt.Println(s1)
s2 := []int{100, 200, 300, 400}
// slice = append(slice, anotherSlice...)
// ... 可变参数 ...xxx
// [...] 根据长度变化数组的大小定义
// anotherSlice... , slice...解构,可以直接获取到slice中的所有元素
// s2... = {100,200,300,400}
s1 = append(s1, s2...)
//遍历切片
for i := 0; i < len(s1); i++ {
fmt.Println(s1[i])
}
for i := range s1 {
fmt.Println(s1[i])
}
}
④ 遍历切片
切片的遍历方式和数组相同,可以使用for
循环或for range
循环,因为本质上切片相当于是引用了数组,数组是一种数据类型,切片引用它,扩展了它,因此遍历方式没太大差别。
slice := []int{1, 2, 3, 4, 5}
for _, value := range slice {
fmt.Println(value)
}
⑤ 扩容的内存分析
切片的扩容会导致内存重新分配及旧数据的复制,这可能影响性能。合理规划初始容量可以减少扩容的次数,提升性能。
slice := make([]int, 0, 1024) // 提前分配足够的容量
案例:切片扩容的内存变化
package main
import "fmt"
// 切片扩容的内存分析
// 结论
// 1、每个切片引用了一个底层的数组
// 2、切片本身不存储任何数据,都是底层的数组来存储的,所以修改了切片也就是修改了这个数组中的数据
// 3、向切片中添加数据的时候,如果没有超过容量,直接添加,如果超过了容量,会自动扩容,成倍增加,copy
// - 分析程序的原理
// - 看源码
// 4、切片一旦扩容,就是重新指向一个新的底层数组
func main() {
//1、cap每次是成倍增加的
//2、只要容量扩容后,地址就会发生变化
s1 := []int{1, 2, 3}
fmt.Println(s1)
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:3,cap:3
fmt.Printf("%p\n", s1) //0xc000016108
s1 = append(s1, 4, 5)
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:5,cap:6
fmt.Printf("%p\n", s1) //0xc000010390
s1 = append(s1, 6, 7, 8)
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:8,cap:12
fmt.Printf("%p\n", s1) //0xc00005e060
s1 = append(s1, 9, 10)
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:10,cap:12
fmt.Printf("%p\n", s1) //0xc00005e060
s1 = append(s1, 11, 12, 13, 14)
fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:14,cap:24
fmt.Printf("%p\n", s1) //0xc00010c000
}
案例2:copy方法
package main
import "fmt"
// copy方法
func main() {
numbers := []int{1, 2, 3}
fmt.Printf("len=%d,cap=%d,slice=%v\n", len(numbers), cap(numbers), numbers)
//方法一:直接使用make创建切片扩容
numbers2 := make([]int, len(numbers), cap(numbers)*2)
//将原来的底层数据的值拷贝到新的数组中
//func copy(dst,src []Type)int
copy(numbers2, numbers)
fmt.Printf("len=%d,cap=%d,slice=%v\n", len(numbers2), cap(numbers2), numbers2)
}
3、小结
数组
- 一组数
- 数组是值传递的
- 创建数组 [size]int
- 数组的大小事不可变的
- 二维数组,数组套数组
- 冒泡排序
切片
- 切片本事是不存在数据的
- 底层是指向了一个数组
- 如果我们存放的数据大于切片的容量,在底层就会扩容 copy > 1024 1.25倍
很感谢你能看到这里,如有相关疑问,还请下方评论留言。
Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)
希望本篇内容能对大家有所帮助,如果大家喜欢的话,请动动手点个赞和关注吧,非常感谢你们的支持!