diff --git a/day02/array/README.md b/day02/array/README.md index 34aa0dd..5a41580 100644 --- a/day02/array/README.md +++ b/day02/array/README.md @@ -90,4 +90,33 @@ func main() { 1. 循环遍历(for) ```go -``` \ No newline at end of file +``` + + +## 数组作为函数参数 + + +## 多维数组 + + +## 作业 + +请同学们完成以下数组练习题,以巩固对Go语言数组的理解: + +1. **定义和初始化数组** + 定义一个包含5个整数的数组,初始化为1到5,并打印数组内容。 + +2. **数组遍历** + 使用for循环遍历一个字符串数组,打印每个元素的索引和值。 + +3. **数组拷贝** + 创建一个数组,拷贝到另一个数组,修改原数组的第一个元素,观察拷贝后的数组是否改变。 + +4. **多维数组** + 定义一个3x3的整数二维数组,初始化为九宫格形式(1到9),并打印出来。 + +5. **数组作为函数参数** + 编写一个函数,接受一个整数数组作为参数,计算并返回数组中所有元素的和。 + +请将代码写在 `main.go` 文件中,并运行测试。 + diff --git a/day02/map/README.md b/day02/map/README.md new file mode 100644 index 0000000..1568777 --- /dev/null +++ b/day02/map/README.md @@ -0,0 +1,236 @@ +# Map + +Go中的map是一种无序的键值对集合,底层基于哈希表实现。每个键必须是可比较的类型,值可以是任意类型。map提供快速的查找、插入和删除操作。 + +## 核心概念 + +map的底层实现是哈希表,通过键的哈希值快速定位值。键必须是可哈希的类型(如int、string、指针等),值可以是任意类型。map是引用类型,赋值时共享底层数据。 + +```go +// map的零值是nil,不能直接使用 +var m map[string]int // m == nil +``` + +## 创建和初始化 + +**原理**:使用make创建map,指定初始容量可以提高性能。也可以使用字面量初始化。 + +```go +// 使用make创建 +m1 := make(map[string]int) // 空map +m2 := make(map[string]int, 10) // 指定初始容量 + +// 字面量初始化 +m3 := map[string]int{ + "apple": 5, + "banana": 3, +} +fmt.Println(m3) // map[apple:5 banana:3] +``` + +## 访问元素 + +**原理**:通过键访问值,如果键不存在,返回零值。可以使用第二个返回值检查键是否存在。 + +```go +m := map[string]int{"a": 1, "b": 2} + +value := m["a"] // 1 +fmt.Println(value) + +// 检查键是否存在 +if val, exists := m["c"]; exists { + fmt.Println("存在:", val) +} else { + fmt.Println("不存在") +} +``` + +## 修改和添加元素 + +**原理**:直接赋值键即可添加或修改。如果键存在则修改,不存在则添加。 + +```go +m := make(map[string]int) + +m["key1"] = 10 // 添加 +m["key1"] = 20 // 修改 +fmt.Println(m) // map[key1:20] +``` + +## 删除元素 + +**原理**:使用delete函数删除键值对。如果键不存在,delete不报错。 + +```go +m := map[string]int{"a": 1, "b": 2, "c": 3} + +delete(m, "b") // 删除键"b" +fmt.Println(m) // map[a:1 c:3] + +delete(m, "d") // 删除不存在的键,无错误 +``` + +## 遍历map + +**原理**:使用range遍历键值对。遍历顺序是随机的,不能依赖顺序。 + +```go +m := map[string]int{"a": 1, "b": 2, "c": 3} + +for key, value := range m { + fmt.Printf("key: %s, value: %d\n", key, value) +} + +// 只遍历键 +for key := range m { + fmt.Println(key) +} +``` + +## map是引用类型 + +**原理**:map是引用类型,赋值时共享底层数据。修改一个会影响另一个。 + +```go +m1 := map[string]int{"a": 1} +m2 := m1 // 共享底层数据 + +m2["b"] = 2 +fmt.Println(m1) // map[a:1 b:2] m1也被修改 +fmt.Println(m2) // map[a:1 b:2] +``` + +## map作为函数参数 + +**原理**:map作为参数传递时,是引用传递,函数内修改会影响调用者。 + +```go +func addEntry(m map[string]int, key string, value int) { + m[key] = value +} + +func main() { + m := make(map[string]int) + addEntry(m, "new", 42) + fmt.Println(m) // map[new:42] +} +``` + +## 扩展 Map的底层原理 + +从Go 1.23开始,map的底层实现从传统的哈希表改为基于Swiss table的高性能实现,大幅提升了性能和内存效率。以下详细讲解其工作原理。 + +### 1. Swiss Table结构 + +Swiss table是一种现代的开放寻址哈希表设计,具有以下特点: +- 每个桶包含一个控制字节数组和数据数组 +- 控制字节存储元数据(哈希值的高位、删除标记等) +- 数据与控制字节分离,减少缓存未命中 + +``` +Swiss Table结构示意图: ++-------------------+ +-------------------+ +| 控制字节 (8字节) | | 数据槽位 (8个) | +| [H1|H2|H3|H4|...]| | [k1,v1] [k2,v2]...| ++-------------------+ +-------------------+ +| 桶0: 控制元数据 | --> | 桶0: 键值对数据 | ++-------------------+ +-------------------+ +| 桶1: 控制元数据 | --> | 桶1: 键值对数据 | ++-------------------+ +-------------------+ +``` + +### 2. 哈希计算和定位 + +**改进的哈希计算**: +- 使用更快的哈希函数(AesHash或类似) +- 哈希值分为高位和低位 +- 高位用于SIMD比较,低位用于定位 + +``` +哈希计算过程: +Key -> hash(key) -> [高7位: SIMD比较] [低位: 位置索引] +``` + +### 3. 插入和查找过程 + +**查找优化**: +1. 计算哈希值 +2. 使用SIMD指令并行比较控制字节 +3. 快速定位匹配的槽位 + +**插入优化**: +1. 查找空闲槽位 +2. 使用线性探测或二次探测 +3. 写入控制字节和数据 + +``` +查找示意图(使用SIMD): +Hash(Key) = 0xABCD1234 +控制字节 = 0xAB (高位) +位置索引 = 0x1234 & 掩码 + +控制字节数组: [AB|CD|EF|00|...] + ↑ SIMD比较找到匹配 +数据数组: [k1,v1] [k2,v2] [k3,v3] ... +``` + +### 4. 扩容机制 + +**渐进式扩容**: +- 当负载因子超过阈值时触发扩容 +- 新表大小为2的幂次 +- 每次操作迁移少量数据 +- 支持就地扩容(in-place rehashing) + +``` +扩容过程: +旧表: 控制字节 [A|B|C|D] --> 数据 [k1,v1] [k2,v2] [k3,v3] [k4,v4] + +新表: 控制字节 [A|0|B|0|C|0|D|0] --> 数据 [k1,v1] [k2,v2] [k3,v3] [k4,v4] + (插入空字节分隔) +``` + +### 5. 性能优势 + +**相比传统哈希表**: +- **更快的查找**:SIMD并行比较,减少分支预测失败 +- **更好的缓存利用**:控制字节和数据分离 +- **更低的内存开销**:更紧凑的布局 +- **抗哈希冲突**:更好的分布减少最坏情况 + +``` +性能对比: +操作 | 传统哈希表 | Swiss Table +查找 | O(1) avg | O(1) faster +插入 | O(1) avg | O(1) faster +内存使用 | ~32字节/对 | ~24字节/对 +``` + +## 总结 + +map是Go中常用的数据结构,适合需要快速查找的场景。记住map是无序的,键必须可哈希,操作是引用传递的。 + +## 作业 + +请同学们完成以下map练习题,以巩固对Go语言map的理解: + +1. **创建和初始化map** + 创建一个map,键为string,值为int,初始化包含3个键值对,并打印map。 + +2. **访问元素** + 给定map,检查某个键是否存在,如果存在打印值,否则打印"不存在"。 + +3. **修改和添加** + 从空map开始,添加5个键值对,然后修改其中一个值。 + +4. **删除元素** + 删除map中的一个元素,并打印删除前后的map。 + +5. **遍历map** + 遍历map,计算所有值的和。 + +6. **函数参数** + 编写一个函数,接受map作为参数,添加一个新键值对。 + +请将代码写在 `main.go` 文件中,并运行测试。 \ No newline at end of file diff --git a/day02/slice/README.md b/day02/slice/README.md new file mode 100644 index 0000000..7662aa0 --- /dev/null +++ b/day02/slice/README.md @@ -0,0 +1,205 @@ +# 切片 + +![alt text](image.png) + +Go中的slice依赖于数组,它的底层就是数组,所以数组具有的优点, slice都有。 且slice支持可以通过append向slice中追加元素,长度不够时会动态扩展,通过再次slice切片,可以得到得到更小的slice结构,可以迭代、遍历等 + +// runtime/slice.go +type slice struct { + array unsafe.Pointer // 数组指针 + len int // 长度 + cap 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 +``` + +![alt text](image-1.png) + + +## 创建和初始化 + +**原理**:切片是动态数组,底层基于固定大小的数组,可以根据需要动态扩容。创建时可以指定长度和容量。 + +```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] +``` + +## 通过切片创建新的切片 + +**原理**:切片操作创建新的切片视图,共享底层数组。新切片的长度是high-low,容量是原容量减去low。 + +```go +s := []int{0, 1, 2, 3, 4, 5} + +// 创建子切片:s[low:high] (不包括high) +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] 复制整个切片 +``` + +## 遍历切片 + +**原理**:可以使用传统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] +``` + +## 切片作为函数参数 + +**原理**:切片作为参数传递时,是引用传递,函数内修改会影响调用者。高效但需注意副作用。 + +```go +func sum(nums []int) int { + total := 0 + for _, num := range nums { + total += num + } + return total +} + +func main() { + s := []int{1, 2, 3, 4, 5} + result := sum(s) + fmt.Println(result) // 15 +} +``` + +## 总结 +切片是 Golang 中比较有特色的一种数据类型,既为我们操作集合类型的数据提供了便利的方式,是又能够高效的在函数间进行传递,因此在代码中切片类型被使用的相当广泛 + + +## 作业 + +请同学们完成以下切片练习题,以巩固对Go语言切片的理解: + +1. **创建和初始化切片** + 创建一个字符串切片,包含"Go", "Python", "Java",并打印长度和容量。 + +2. **切片操作** + 给定切片[]int{1,2,3,4,5},创建子切片包含第2到第4个元素(不包括第4个),并打印结果。 + +3. **添加元素** + 从空切片开始,使用append添加5个整数,观察长度和容量的变化。 + +4. **遍历切片** + 遍历一个整数切片,计算所有元素的和。 + +5. **切片拷贝** + 创建一个切片,拷贝到另一个切片,修改原切片,验证拷贝是否独立。 + +6. **函数参数** + 编写一个函数,接受字符串切片作为参数,将所有字符串转换为大写并返回新切片。 + +请将代码写在 `main.go` 文件中,并运行测试。 diff --git a/day02/slice/image-1.png b/day02/slice/image-1.png new file mode 100644 index 0000000..501f17d Binary files /dev/null and b/day02/slice/image-1.png differ diff --git a/day02/slice/image.png b/day02/slice/image.png new file mode 100644 index 0000000..1a899c2 Binary files /dev/null and b/day02/slice/image.png differ diff --git a/day02/struct/README.md b/day02/struct/README.md index 9c0aadd..259f1e9 100644 --- a/day02/struct/README.md +++ b/day02/struct/README.md @@ -1,7 +1,18 @@ # 结构体 +![alt text](image.png) -## 子针数组(重要: 95%) +我们前面介绍的数组 只能保存同一种类型的数据, 当我们需要记录多种不同类型的数据时,我们该怎么办? + +结构体就是用于解决这个问题的, 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合, 方便容量我们的任意类型的数据 + + +## 结构体的定义 + + + + +## 指针数组(重要: 95%) 指针数组: 创建一个数组,里面存放指针,指针指向数组的某一个元素 @@ -16,5 +27,5 @@ type Person struct {} ``` -## 子针数组的copy +## 指针数组的拷贝 diff --git a/day02/struct/image.png b/day02/struct/image.png new file mode 100644 index 0000000..1aee4a0 Binary files /dev/null and b/day02/struct/image.png differ