冲突解决
- 开放寻址
- hash初始化的时候,先创建一个数组array
- 通过hashfunc去找对应的数组的位置,比如key=a,对应0的位置
- 假设key=p的位置也是0,但此时0的位置已经被a占用,那只能向后继续找,直至找到一个空的坑位。
优点:实现相对简单
缺点:装载因子(元素数量/数组长度)过大(超过70%)时,效率很差。比如当填满时,元素z本该在数组第一位置的时候,确因为冲突放在了数组的最后,那么查询元素z的时候,就必须一直向后找,时间复杂度就是O(N)。删除一个元素的时候,也不是真正的删除。因为数组空间的连续性,即使元素没了,坑位还是在那。
- 链表 通过链表来解决冲突是当下大多数语言map的实现方式
- 初始化桶的个数
- 通过hash,判断key应该在哪个桶里
- 如果多个key分到同一个桶那就是冲突,通过链表的方式,把冲突的key连接在一起
优点:冲突的时候,可以通过链表的方式把元素串起来,不需要向开放寻址那样申请连续的内存空间。
缺点:桶的数目不能过多(需要申请一大块连续空间,删除的元素的时候,碎片多,浪费),桶的数目不能太少(大部分元素都在一个桶里,元素多的时候,查找的时间复杂度接近O(N)。
hmap
golang的map数据结构就是这种hmap
- count:键值对数目
- flags:状态标识,比如是否在被写或者迁移等,因为map不是线程安全的所以操作时需要判断flags
- noverflow:溢出桶使用的数量
- hash0:hash 种子,做key 哈希的时候会用到
- B:桶的数目(2^B)
- buckets:桶存放的位置
- oldbuckets:旧桶的位置(扩容时用到)
- nevacuate:迁移的进度,小于此地址的bucket已经迁移完
- extra:溢出桶相关
为什么是2^B次方的桶,难道6个桶、7个桶不行?
hash的方式目前主要就两种
- 取模法 100%30=10这种
- 与运算 hash & (2^B-1),假设B是2(3个桶0,1,2)那么 hash&0011,理论每个桶都有概率被选中。 如果桶的数目是5(0101)那么低位的第二位始终是0,始终有桶是空桶(例如3号桶011)
bmap
- key高8位的slice,通过对比高8位,来快速查找目标数据。
- 一个bmap 可以存8个k/v,为了使内存更紧凑,8key在一起,8个value在一起, 假设key、value都是占两个字节的,大致如下图
扩容
hmap中的mapextra
- overflow:使用的溢出桶的地址集合
- oldoverflow:迁移过程中老的溢出桶的地址
- nextOverflow:指向下一个空闲的溢出桶地址
溢出桶的创建
在创建map的时候如果B>=4,那么认为会使用到溢出桶的概率比较大,就会创建2^(B-4)个溢出桶,在内存上和常规的hmap是连续的
扩容规则
- 翻倍扩容:当负载因子count/2^B > 6.5
- 等量扩容:当溢出桶较多:
- B<=15 noverflow>=2^B
- B>15 noverflow>=2^15 等量扩容的意义在于,当bmap中的元素分布的不紧凑(存在删除元素时),需要重新紧凑下元素。
range map为什么无序
每次都会随机一个桶的位置,然后遍历bmap,再遍历溢出桶,在找下一个桶...