docs(slice): 更新切片文档添加冒泡排序示例和图解 - 添加切片作为函数参数的详细说明,包含值传递和引用传递的对比 - 实现MySort(值传递)和MySortV2(引用传递)两个版本的冒泡排序函数 - 新增draw.io图表文件cut.drawio展示切片切割的可视化解释 - 在README.md中添加图片引用和更详细的切片函数参数说明 - 完善main.go中的切片操作示例,包括切割、删除和排序演示 - 修正切片作业描述,明确要求实现两个版本的字符串转大写函数 ```
253 lines
6.9 KiB
Markdown
253 lines
6.9 KiB
Markdown
# 切片(slice)
|
||
|
||

|
||
|
||
Go中的slice依赖于数组,它的底层就是数组,所以数组具有的优点, slice都有。 且slice支持可以通过append向slice中追加元素,长度不够时会动态扩展,通过再次slice切片,可以得到得到更小的slice结构,可以迭代、遍历等
|
||
|
||
基于数组的 复合数据结构
|
||
|
||
```go
|
||
// runtime/slice.go
|
||
type slice struct {
|
||
array unsafe.Pointer // 数组指针
|
||
cap int // 容量
|
||
len int // 长度
|
||
|
||
}
|
||
```
|
||
|
||
每一个slice结构都由3部分组成:
|
||
+ 容量(capacity): 即底层数组的长度,表示这个slice目前最多能扩展到这么长
|
||
+ 长度(length):表示slice当前的长度,即当前容纳的元素个数
|
||
+ 数组指针(array): 指向底层数组的指针
|
||
|
||
比如创建一个长度为3,容量为5,int类型的切片
|
||
```go
|
||
s := make([]int, 3, 4)
|
||
fmt.Println(a, len(s), cap(s)) // [0 0 0] 3 5
|
||
```
|
||
|
||

|
||
|
||
## 切片与数组
|
||
|
||
为了灵活(不需要制定长度, 自动扩容)
|
||
|
||
```go
|
||
func main() {
|
||
// 1. 声明切片, 切片不是一个值,是一个boxed结构体, array unsafe.Pointer // 数组指针
|
||
// 底层数组的长度: 容量10, 当前有几个元素3
|
||
slice1 := make([]int, 3, 5)
|
||
fmt.Println(slice1, len(slice1), cap(slice1))
|
||
// 底层数组的长度: 容量10, 当前有几个元素3
|
||
slice1 = append(slice1, 4, 5)
|
||
fmt.Println(slice1, len(slice1), cap(slice1))
|
||
// 这里的容器, 不是硬性限制,是超过容量后,底层数组是自动扩容的(重要)
|
||
// 扩容: 一般是原来的2倍, 新申请一块更大的数组, 把老数据copy过去
|
||
slice1 = append(slice1, 6)
|
||
fmt.Println(slice1, len(slice1), cap(slice1))
|
||
}
|
||
```
|
||
|
||
## 创建和初始化
|
||
|
||
**原理**:切片是动态数组,底层基于固定大小的数组,可以根据需要动态扩容。创建时可以指定长度和容量。
|
||
|
||
```go
|
||
// 使用 make 创建切片:make([]类型, 长度, 容量)
|
||
s1 := make([]int, 3, 5) // 长度3,容量5
|
||
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0] 3 5
|
||
|
||
// 直接初始化
|
||
s2 := []int{1, 2, 3} // 长度和容量都是3
|
||
fmt.Println(s2, len(s2), cap(s2)) // [1 2 3] 3 3
|
||
|
||
// 从数组创建切片
|
||
arr := [5]int{1, 2, 3, 4, 5}
|
||
s3 := arr[1:4] // 从索引1到3(不包括4)
|
||
fmt.Println(s3, len(s3), cap(s3)) // [2 3 4] 3 4
|
||
```
|
||
|
||
## 切片访问
|
||
|
||
**原理**:切片通过索引访问元素,索引从0开始。与数组类似,但更灵活。
|
||
|
||
```go
|
||
s := []int{10, 20, 30, 40, 50}
|
||
|
||
// 通过索引访问元素
|
||
fmt.Println(s[0]) // 10
|
||
fmt.Println(s[2]) // 30
|
||
|
||
// 修改元素
|
||
s[1] = 25
|
||
fmt.Println(s) // [10 25 30 40 50]
|
||
```
|
||
|
||
## nil和空切片
|
||
|
||
**原理**:nil切片表示未初始化的切片,长度和容量都为0。空切片是已初始化的切片但不包含元素。两者在行为上略有不同。
|
||
|
||
```go
|
||
// nil 切片:未初始化的切片
|
||
var s1 []int
|
||
fmt.Println(s1 == nil) // true
|
||
fmt.Println(len(s1), cap(s1)) // 0 0
|
||
|
||
// 空切片:已初始化的空切片
|
||
s2 := []int{}
|
||
s3 := make([]int, 0)
|
||
fmt.Println(len(s2), cap(s2)) // 0 0
|
||
fmt.Println(len(s3), cap(s3)) // 0 0
|
||
```
|
||
|
||
## 往切片中添加元素
|
||
|
||
**原理**:使用append函数添加元素。如果容量不足,会自动扩容底层数组,通常容量翻倍增长。
|
||
|
||
```go
|
||
s := []int{1, 2}
|
||
fmt.Println(s, len(s), cap(s)) // [1 2] 2 2
|
||
|
||
// 使用 append 添加元素
|
||
s = append(s, 3)
|
||
fmt.Println(s, len(s), cap(s)) // [1 2 3] 3 4 (容量自动扩容)
|
||
|
||
// 添加多个元素
|
||
s = append(s, 4, 5)
|
||
fmt.Println(s) // [1 2 3 4 5]
|
||
```
|
||
|
||

|
||
|
||
## 遍历切片
|
||
|
||
**原理**:可以使用传统for循环或range关键字遍历。range返回索引和值,更简洁。
|
||
|
||
```go
|
||
s := []string{"apple", "banana", "cherry"}
|
||
|
||
// 方法1:使用索引
|
||
for i := 0; i < len(s); i++ {
|
||
fmt.Println(i, s[i])
|
||
}
|
||
|
||
// 方法2:使用 range(推荐)
|
||
for index, value := range s {
|
||
fmt.Println(index, value)
|
||
}
|
||
```
|
||
|
||
## slice是引用类型
|
||
|
||
**原理**:切片是引用类型,赋值时复制切片头,但共享底层数组。修改一个会影响另一个。
|
||
|
||
```go
|
||
s1 := []int{1, 2, 3}
|
||
s2 := s1 // s2 指向同一个底层数组
|
||
|
||
s2[0] = 99
|
||
fmt.Println(s1) // [99 2 3] s1 也被修改了
|
||
fmt.Println(s2) // [99 2 3]
|
||
```
|
||
|
||

|
||
|
||
|
||
## 切片拷贝
|
||
|
||
**原理**:使用copy函数进行深拷贝,创建独立的数据副本。修改拷贝后的切片不会影响原切片。
|
||
|
||
```go
|
||
s1 := []int{1, 2, 3}
|
||
s2 := make([]int, len(s1))
|
||
copy(s2, s1) // 拷贝元素到 s2
|
||
|
||
s2[0] = 99
|
||
fmt.Println(s1) // [1 2 3] s1 不变
|
||
fmt.Println(s2) // [99 2 3]
|
||
```
|
||
|
||

|
||
|
||
## 通过切片创建新的切片
|
||
|
||
**原理**:切片操作创建新的切片视图,共享底层数组。新切片的长度是high-low,容量是原容量减去low。
|
||
|
||
```go
|
||
s := []int{0, 1, 2, 3, 4, 5}
|
||
|
||
// 创建子切片:s[low:high] (不包括high) [start: end)
|
||
sub1 := s[1:4] // [1, 2, 3]
|
||
fmt.Println(sub1)
|
||
|
||
// 省略索引
|
||
sub2 := s[:3] // [0, 1, 2]
|
||
sub3 := s[2:] // [2, 3, 4, 5]
|
||
sub4 := s[:] // [0, 1, 2, 3, 4, 5] 复制整个切片
|
||
```
|
||
|
||

|
||
|
||
## 切片作为函数参数 (重点: 90%)
|
||
|
||
**原理**:切片作为参数传递时,是引用传递,函数内修改会影响调用者。高效但需注意副作用。
|
||
|
||
```go
|
||
// 冒泡排序: 值传递
|
||
// 返回一个新的切片(排序的结果)
|
||
func MySort(arr []int) []int {
|
||
// 冒泡排序
|
||
for i := 0; i < len(arr)-1; i++ {
|
||
for j := 0; j < len(arr)-1-i; j++ {
|
||
if arr[j] > arr[j+1] {
|
||
// 值交换: a, b = b, a
|
||
arr[j], arr[j+1] = arr[j+1], arr[j]
|
||
}
|
||
}
|
||
}
|
||
return arr
|
||
}
|
||
|
||
// 冒泡排序: 引用传递
|
||
// 直接修改传入的切片(在原切片上进行排序)
|
||
func MySortV2(arr *[]int) {
|
||
// 冒泡排序
|
||
for i := 0; i < len(*arr)-1; i++ {
|
||
for j := 0; j < len(*arr)-1-i; j++ {
|
||
if (*arr)[j] > (*arr)[j+1] {
|
||
// 值交换: a, b = b, a
|
||
(*arr)[j], (*arr)[j+1] = (*arr)[j+1], (*arr)[j]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 总结
|
||
切片是 Golang 中比较有特色的一种数据类型,既为我们操作集合类型的数据提供了便利的方式,是又能够高效的在函数间进行传递,因此在代码中切片类型被使用的相当广泛
|
||
|
||
## 作业
|
||
|
||
请同学们完成以下切片练习题,以巩固对Go语言切片的理解:
|
||
|
||
1. **创建和初始化切片**
|
||
创建一个字符串切片,包含"Go", "Python", "Java",并打印长度和容量。
|
||
|
||
2. **切片操作**
|
||
给定切片[]int{1,2,3,4,5},创建子切片包含第2到第4个元素(不包括第4个),并打印结果。
|
||
|
||
3. **添加元素**
|
||
从空切片开始,使用append添加5个整数,观察长度和容量的变化。
|
||
|
||
4. **遍历切片**
|
||
遍历一个整数切片,计算所有元素的和。
|
||
|
||
5. **切片拷贝**
|
||
创建一个切片,拷贝到另一个切片,修改原切片,验证拷贝是否独立。
|
||
|
||
6. **函数参数**
|
||
编写一个函数,接受字符串切片作为参数,将所有字符串转换为大写并返回新切片, 需要实现2个版本, 1.一个值版本, 2. 指针版本。
|
||
|
||
请将代码写在 `main.go` 文件中,并运行测试。
|