MAP
map的底层是哈希表,哈希表拥有O(1)的读写性能
哈希表的两个关键点是哈希函数和哈希冲突解决方法
哈希函数
理想情况下,哈希函数应该能够将不同键映射到不同的索引上,不过在实际使用时,这个理想的结果是不可能实现的
理想情况:
不均匀的哈希函数:
冲突解决
开放寻址法
如果采用开放寻址法实现哈希表,那么支撑哈希表的数据结构就是数组,数组的长度有限,存储(author, draven)这个键值对会从如下的索引开始遍历:
index := hash("author")%array.len
当向哈希表写入新的数据时发生了冲突,就会将键值对写入到下一个不为空的位置
Key3与key1和key2发生冲突时,key3被写入key2后面的空闲内存中;当读取key3对应的值时,会先对键进行哈希取模,这会帮助我们找到Key1,Key1与Key3不匹配,继续查找后面的元素,直到内存为空或者找到目标元素
拉链法
拉链法的实现比较开放寻址法稍微复杂一些,但是平均查找的长度也比较短,各个用于存储节点的内存都是动态申请的,可以节省比较多的存储空间
实现拉链法一般会使用数组加上链表,拉链法会使用链表数组作为哈希底层的数据结构,类似一个可扩展的“二维数组”
当需要将一个键值对(key6, value6)写入哈希表时,键值对中的键key6会经过一个哈希函数,哈希函数返回的哈希会帮助我们选择一个桶,和开放寻址法一样,选择桶的方式就是直接对哈希返回的结果取模
当选择了2号桶之后就可以遍历桶中的链表了,在遍历链表的过程中会遇到以下两种情况:
- 找到键相同的键值对:更新键对应的值
- 没有找到键相同的键值对:在链表的末尾追加新键值对
当查找某个键时,哈希表发现它命中的桶,依次遍历桶中的链表,如果遍历到链表的末尾也没有找到希望的键,表明哈希表中没有该键对应的值
装载因子 := 元素数量 / 桶数量
数据结构
Go语言使用hmap结构体来表示哈希
- count表示当前哈希表中的元素数量
- B表示当前哈希表持有的buckets(桶)数量,哈希表中桶的数量都是2的倍数,所以该字段会存储对数,
len(buckets) == 2^B - hash0是哈希的种子,它为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
- oldbuckets是哈希扩容时用于保存之前buckets的字段,它的大小是当前buckets的一半
哈希表hmap的桶bmap,每一个bmap都能存储8个键值对,当哈希表中存储的数据过多,单个桶无法装满时就会使用overflow中的桶存储溢出的数据,上图中黄色的bmap就是正常桶,绿色的是溢出桶,溢出桶是为了减少扩容的频率
扩容
在以下两种情况发生时触发哈希的扩容:
- 装载因子已经超过了6.5
- 哈希使用了太多溢出桶;由于Go语言哈希的扩容不是一个原子的过程,再扩容前需要判断当前哈希是否已经处于扩容状态,避免二次扩容造成混乱