学习笔记golang map

998 阅读3分钟

冲突解决

  • 开放寻址
  1. hash初始化的时候,先创建一个数组array
  2. 通过hashfunc去找对应的数组的位置,比如key=a,对应0的位置
  3. 假设key=p的位置也是0,但此时0的位置已经被a占用,那只能向后继续找,直至找到一个空的坑位。

image.png

优点:实现相对简单
缺点:装载因子(元素数量/数组长度)过大(超过70%)时,效率很差。比如当填满时,元素z本该在数组第一位置的时候,确因为冲突放在了数组的最后,那么查询元素z的时候,就必须一直向后找,时间复杂度就是O(N)。删除一个元素的时候,也不是真正的删除。因为数组空间的连续性,即使元素没了,坑位还是在那。

  • 链表 通过链表来解决冲突是当下大多数语言map的实现方式
  1. 初始化桶的个数
  2. 通过hash,判断key应该在哪个桶里
  3. 如果多个key分到同一个桶那就是冲突,通过链表的方式,把冲突的key连接在一起

image.png 优点:冲突的时候,可以通过链表的方式把元素串起来,不需要向开放寻址那样申请连续的内存空间。
缺点:桶的数目不能过多(需要申请一大块连续空间,删除的元素的时候,碎片多,浪费),桶的数目不能太少(大部分元素都在一个桶里,元素多的时候,查找的时间复杂度接近O(N)。

hmap

golang的map数据结构就是这种hmap carbon.png

  • count:键值对数目
  • flags:状态标识,比如是否在被写或者迁移等,因为map不是线程安全的所以操作时需要判断flags
  • noverflow:溢出桶使用的数量
  • hash0:hash 种子,做key 哈希的时候会用到
  • B:桶的数目(2^B)
  • buckets:桶存放的位置
  • oldbuckets:旧桶的位置(扩容时用到)
  • nevacuate:迁移的进度,小于此地址的bucket已经迁移完
  • extra:溢出桶相关

为什么是2^B次方的桶,难道6个桶、7个桶不行?

hash的方式目前主要就两种

  1. 取模法 100%30=10这种
  2. 与运算 hash & (2^B-1),假设B是2(3个桶0,1,2)那么 hash&0011,理论每个桶都有概率被选中。 如果桶的数目是5(0101)那么低位的第二位始终是0,始终有桶是空桶(例如3号桶011

bmap

carbon (2).png

  • key高8位的slice,通过对比高8位,来快速查找目标数据。
  • 一个bmap 可以存8个k/v,为了使内存更紧凑,8key在一起,8个value在一起, 假设key、value都是占两个字节的,大致如下图

image.png

扩容

hmap中的mapextra

carbon (3).png

  • overflow:使用的溢出桶的地址集合
  • oldoverflow:迁移过程中老的溢出桶的地址
  • nextOverflow:指向下一个空闲的溢出桶地址

溢出桶的创建

在创建map的时候如果B>=4,那么认为会使用到溢出桶的概率比较大,就会创建2^(B-4)个溢出桶,在内存上和常规的hmap是连续的

image.png

扩容规则

  • 翻倍扩容:当负载因子count/2^B > 6.5
  • 等量扩容:当溢出桶较多:
  1. B<=15 noverflow>=2^B
  2. B>15 noverflow>=2^15 等量扩容的意义在于,当bmap中的元素分布的不紧凑(存在删除元素时),需要重新紧凑下元素。

range map为什么无序

carbon (4).png 每次都会随机一个桶的位置,然后遍历bmap,再遍历溢出桶,在找下一个桶...