map
map是一种无序的基于key-value
的数据结构
哈希表
提到key-value
就会想到哈希表,哈希表通常会有一堆桶来存储键值对,一个键值对来了,会选择一个桶。
有两种方法比较常用:
1.取模法 hash%m 用hash值与桶的个数m取模
2.与运算 hash&(m-1) 哈希值与m-1进行与运算,若想运算结果在区间内,并且不会出现空桶则m必须是2的整数次幂
如果之后还有键值对选择这个桶,就是发生了哈希冲突,解决冲突的办法,常用的有两种:
1.开放地址法,找到被占用的桶后面没被占用的桶来用。
2.拉链法, 在被占用的桶后面链一个新桶存储这个键值对。
哈希冲突的发生会使哈希表的读写效率降低,选择散列均匀的哈希函数可以减少哈希冲突的发生,适时对哈希表进行扩容也是保障读写效率的有效手段,通常会把存储键值对的数目与桶的数目的比值作为是否需要扩容的判断依据,这个比值被称为负载因子:Load Factor = count/m
分配新桶时,需要把旧桶里存储的键值对都迁移到新桶中,如果存储的键值对信息较多,一次性迁移所有桶花费的时间就比较明显著,所以通常会在哈希表扩容时,先分配足够多的新桶,增加一个字段记录旧桶的位置,再增加一个字段记录旧桶迁移的进度,在哈希表每次读写操作的时候,如果检测到当前正处于扩容阶段,就完成一部分键值迁移任务,直到所有迁移完成,旧桶不再使用,才算真正完成一次哈希表的扩容,这就是渐进式扩容,可以避免一次性扩容带来的性能瞬时抖动。
Go语言中的map
GO语言中Map类型的底层实现就是哈希表
type hmap struct{
count int //键值对数目
flags uint8
B uint8 //桶的数目2^B个
noverflow uint16 //使用溢出桶的数量
hash0 uint32
buckets unsafe.Pointer //桶
oldbuckets unsafe.Pointer //旧桶
nevacuate uintptr //即将迁移的旧桶编号
extra *mapextra //记录溢出桶的相关信息
}
bmap结构
溢出桶内存布局与常规桶相同,是为了减少扩容次数而引进的,实际上,如果哈希表要分配的桶的数目大于2^4
就认为使用到溢出桶的几率较大,就会预分配2^(B-4)个溢出桶备用,这些溢出桶与常规桶在内存中是连续的。
map扩容规则
当负载因子超过6.5就会触发翻倍扩容,如果负载因子没超标而使用的溢出桶较多则进行等量扩容,当B<=15 noverflow >= 2^B
或者 B > 15 noverflow > 2^15
则溢出桶就算较多了,等量扩容就是创建和旧桶一样多的新桶,然后把键值对迁移过来,这样做的原因是,当桶的负载因子没有超过上限值,却使用了很多溢出桶,只可能是很多键值对被删除的情况。
这样迁移到新桶中,能够排列的更加紧凑,从而减少溢出桶的使用。
map定义
Go语言中 map
的定义语法如下:
map[KeyType]ValueType
其中,
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
map基本使用
map中的数据都是成对出现的,map的基本使用示例代码如下:
func main() {
scoreMap := make