概述
哈希表是工程中常用到的数据类型,能提供快速的检索和更新。复杂度一般为 O(1)
本篇博文分 两部分写,第一部分是源码学习,第二部分是一些内部实现,以及觉着有意思的一些地方,以及个人思考
理论
哈希表需要解决的问题有两个
- 位置索引
- 数据碰撞
索引交给 hash function
哈希算法,常用就是模运算
解决碰撞主要有以下三种方式
- 分离链接,也就是利用链表性质存储冲突的 key,然后通过遍历来区分(单独的存储层面)
- 开放定址
- 线性探测(存储层面以及算法层面都有所调整)
- 平方探测(同上,只是算法层面小改动而已)
- 双散列
- 再散列 (扩容以及数据迁移)
可扩散列是用来解决数据太大而无法装进内存的场景,此处不讨论
哈希表的效率与 load factor
装填因子有关,用来估量其平均复杂度。含义就是一个其计算方式一般就是使用 已经存储的数据量 / 可索引地址的数量。 或者说,单个索引地址的平均长度
碰撞的解决
理想情况下,没有碰撞的时候,使用一个数组,与一个哈希算法就可以实现散列结构。但是碰撞无法完全避免,那么就有了以下几种方式来解决。
分离链接
分离链接的核心是通过使用链表来处理碰撞问题。数组用来做索引,内部存储链表,链表存储的是哈希碰撞的 key 以及 value,存储 key 是为了在冲突的时候,仍旧可以通过比对来实现定位。
开放定址
链表的问题是节点申请,会造成内存的频繁操作。如果在数据量不是特别大的时候,可以考虑开放定址的方式。其仍旧使用一个比较大的数组。只是在发生碰撞的时候,可以通过向固定方向进行偏移来进行存储,从而解决碰撞问题。
线性探测和平方探测就在于偏移量选择上。双散列(略)
再散列
碰撞某种程度上可以说是存储空间较小造成的。那么 rehash 的思想就是,申请更大的空间,然后将数据重新计算,重新定位。
Golang 的 map 实现
golang 中的 map 是一个哈希表,其实现方式使用到了链表以及 rehash。
链表是在用在较小层面碰撞,rehash 则是当 load factor
较大的时候使用的方式。
注意:本篇记录是基于 go 1.9.2
版本记录的。
数据结构
golang 的 map
内并没有直接存储传递进来的 key
和 value
,而是使用了其引用,以及 key 的 hash 值的高位(后面再说)。
下面是 map 数据结构的部分,选取了主要是跟存储相关的域。
type hmap struct {
B uint8
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
extra *mapextra
}
buckets 与 oldbuckets 是指向一段连续地址的指针地址。主要是用来存储 key 和 value 的引用地址,暂时理解成数据部分好了。其中oldbuckets 只有在扩容的时候才会用到。两者与前面『分离链接』实现中的数组功能类似,供初步索引使用。
type mapextra struct {