2026-01-11 13:56:26 +08:00
|
|
|
|
# Map
|
|
|
|
|
|
|
|
|
|
|
|
Go中的map是一种无序的键值对集合,底层基于哈希表实现。每个键必须是可比较的类型,值可以是任意类型。map提供快速的查找、插入和删除操作。
|
|
|
|
|
|
|
2026-01-11 18:09:27 +08:00
|
|
|
|
为啥有了切片,我还有有map
|
|
|
|
|
|
|
|
|
|
|
|
1. 切片如何寻找到指定的对象
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
personList := []Person{}
|
|
|
|
|
|
personList = append(personList, Person{
|
|
|
|
|
|
Name: "张三",
|
|
|
|
|
|
Age: 18,
|
|
|
|
|
|
})
|
|
|
|
|
|
personList = append(personList, Person{
|
|
|
|
|
|
Name: "李四",
|
|
|
|
|
|
Age: 20,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 如何找到李四这个对象
|
|
|
|
|
|
for _, person := range personList {
|
|
|
|
|
|
if person.Name == "李四" {
|
|
|
|
|
|
// 找到了
|
|
|
|
|
|
fmt.Println("找到李四:", person)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果我这个切切片非常大, 上百万, 上千万
|
|
|
|
|
|
// 通过切片查找一个元素, 效率非常低, O(n)
|
|
|
|
|
|
// 这时可以使用 map 来优化查找效率
|
|
|
|
|
|
|
|
|
|
|
|
// 使用map 来存储人员信息, key 是姓名, value 是 Person 对象
|
|
|
|
|
|
personMap := map[string]Person{}
|
|
|
|
|
|
personMap["张三"] = Person{
|
|
|
|
|
|
Name: "张三",
|
|
|
|
|
|
Age: 22,
|
|
|
|
|
|
}
|
|
|
|
|
|
personMap["李四"] = Person{
|
|
|
|
|
|
Name: "李四",
|
|
|
|
|
|
Age: 24,
|
|
|
|
|
|
}
|
|
|
|
|
|
fmt.Println("通过Map直接找到李四: ", personMap["李四"])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Person struct {
|
|
|
|
|
|
Name string
|
|
|
|
|
|
Age int
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
2026-01-11 13:56:26 +08:00
|
|
|
|
## 核心概念
|
|
|
|
|
|
|
|
|
|
|
|
map的底层实现是哈希表,通过键的哈希值快速定位值。键必须是可哈希的类型(如int、string、指针等),值可以是任意类型。map是引用类型,赋值时共享底层数据。
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
// map的零值是nil,不能直接使用
|
|
|
|
|
|
var m map[string]int // m == nil
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-11 18:09:27 +08:00
|
|
|
|
+ 无序的: (hash)
|
|
|
|
|
|
+ key不允许重复 (hash)
|
|
|
|
|
|
|
2026-01-11 13:56:26 +08:00
|
|
|
|
## 创建和初始化
|
|
|
|
|
|
|
|
|
|
|
|
**原理**:使用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]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-11 18:09:27 +08:00
|
|
|
|

|
|
|
|
|
|
|
2026-01-11 13:56:26 +08:00
|
|
|
|
## 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]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-01-11 18:09:27 +08:00
|
|
|
|
## 使用map去重
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-11 13:56:26 +08:00
|
|
|
|
## 扩展 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作为参数,添加一个新键值对。
|
|
|
|
|
|
|
2026-01-11 18:09:27 +08:00
|
|
|
|
7. **去重**
|
|
|
|
|
|
给定一个只能的[]string切片,使用map来进行去重
|
|
|
|
|
|
|
2026-01-11 13:56:26 +08:00
|
|
|
|
请将代码写在 `main.go` 文件中,并运行测试。
|