Map
Go语言实现的map是一种无序的基于键值对(key-value)的数据结构,为引用类型,使用前必须初始化
声明
map类型的变量的初始值默认为nil,直接使用会导致panic
需使用make进行内存分配,完成初始化,也可以直接使用:=完成
func main() {
var mp1 map[int]int
mp1[1] = 1 // panic 只是声明了 但没有分配空间
fmt.Println(mp1[1]) //panic: assignment to entry in nil map
mp2 := map[int]int{}
mp2[1] = 1
fmt.Println(mp2[1]) // 1
mp3 := make(map[int]int)
mp3[1] = 1
fmt.Println(mp3[1]) // 1
}
使用
直接对map[key]进行操作,若key不存在会自动创建,存在则会覆盖value(简单类型)
func main() {
mp := make(map[string]string)
mp["test"] = "hello world"
fmt.Println(mp["test"]) // hello world
}
判断key是否存在
map[key]有两个返回值
val, ok := map[key] val为值,ok是bool类型变量, true则表示此key存在于这个map中,当不存在时,val为对应类型的零值 (string 为 "", int,float32 64为 0 , bool 为false)
func main() {
mp := make(map[string]bool)
mp["test"] = true
if val, ok := mp["test"]; ok {
fmt.Println(val) //true
}
val, ok := mp["test1"]
fmt.Println(val, ok) //false false
}
map的遍历
使用for k, v := range map 语法,其中k为键(key), v为值(value), 不需要某个值时使用_忽略
注意: map是无序的,所以你存储的顺序和输出顺序很大可能是不一致的
func main() {
mp := map[string]int{
"A":100,
"B":90,
"C":80,
}
for _,v := range mp{
fmt.Println(v) //三个数随机输出 因为map是无序的
}
}
要想得到有序结果,只能将值取出来放入切片等结构进行排序得到顺序值
对于不需要的键我们可以使用delete函数进行删除
func main() {
mp := map[string]int{"a": 1, "b": 2, "c": 3}
v, ok := mp["a"]
fmt.Println(v, ok) // 1 true
delete(mp, "a")
v, ok = mp["a"]
fmt.Println(v, ok) // 0 false
}
我们也可以利用map来实现很多的非内置的数据结构,如
使用map实现set(集合 每个元素只能存在一次),利用struct{}空结构体当作值
func main() {
mp := make(map[int]struct{})
nums := []int{1, 5, 3, 6, 7, 3, 2, 7, 4}
for _, num := range nums {
mp[num] = struct{}{}
}
fmt.Println(len(mp)) // 7
}
值也可以为复杂类型,如切片
func main() {
mp := make(map[int][]int)
nums := []int{1, 5, 3, 6, 7, 3, 2, 7, 4}
for i, num := range nums {
mp[num] = append(mp[num], i)
}
fmt.Println(mp[3]) // 2 5
}
此时作用就是记录了每个数字对应的一个或多个相同值的下标
map底层
map底层由哈希表实现,一个hash表中存在多个桶(bucket),每个桶中保存了map中的一个或一组键值对
type hmap struct{
count int //当前保存的元素个数
B //桶的大小
buckets unsafe.Pointer //桶数组 长度是 2 ^ B
oldbuckets unsafe.Pointer //旧桶 用于扩容
}
官方给出的每个bucket可以存储8个键值对
hash冲突是不可避免的,会降低我们的存储效率,Go的map使用链地址法来解决hash冲突,用类似链表的结构将桶串联起来,但其实最坏的情况会导致完全成为一条链
如果冲突过多就会触发rehash的操作(负载因子过高) 负载因子 = 键数量 / 桶数量
- 负载因子过小,空间利用率低
- 负载因子过大,冲突严重,存取效率低(链式查询需要O(n) hash的效率是O(1))
rehash就是解决负载因子过大的一种手段,他会对所有的键值对进行重排,使其均匀的分布在这些桶内
扩容也是降低负载因子的常用手段(空间换时间)
Go的扩容机制很巧妙 利用了旧桶去指向原本的桶位置,新桶会扩容到原本的2倍,采取逐步搬迁的策略,每次访问搬走两个桶,在搬迁过程中,查询会从旧桶开始查询,没有才会访问新桶,而添加则是直接在新桶进行操作