HashMap学习笔记
HashMap 学习笔记
HashMap
是 Java 开发中常用的一种数据接口,常用于完成key:value
结构的存储。而同时,HashMap
又是HashSet
、HashTable
、ConcurrentHashMap
这三种数据结构的基础。
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用
NULL
值和NULL
键。(除了非同步和允许使用NULL
之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的"容量"(桶的数量)及其大小(键-值映射关系数)成比例。
所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
基本结构
HashMap 的基本结构是一个可以扩容的动态数组。该动态数组有着以下属性:
属性值 | 默认值 | 属性说明 |
---|---|---|
capacity | 目前数组的长度。为了实现高效的扩容,其值为的幂次方 | |
loadFactor | 负载因子,该值与threshold 配合控制扩容 | |
threshold | 扩容的阈值,即当数组内达到这么多元素时,会触发数组的扩容 |
数组元素
哈希表看作一个可扩容动态数组,而数组中的每个元素是一个Node
,它的属性有:
元素名称 | 意义 |
---|---|
hash | 当前位置值的 hash 值 |
key | 当前位置的键 |
value | 当前位置存储的值 |
next | 下一个 Node |
链表与树
对于每个 Node 元素,我们发现它有一个next
属性。而通过它,挂载到数组同一个位置的多个 Node 就组成了链表或者树。
在JAVA1.7
阶段,不使用红黑树解决哈希冲突,即发生冲突的多个 Node 构成了一个单向链表(拉链法解决哈希冲突)
而在JAVA 1.8
中,当单项链表中元素大于等于时,单项列表会变为一棵红黑树,链表的查找时间复杂度为,而红黑树查找的时间复杂度为 。因此,在数组的同一位置挂载的节点较多时,JAVA 1.8
的设计会降低时间复杂度。
该转化操作是由以下函数实现的
1 | final void treeifyBin(Node<K,V>[] tab, int hash) |
官方对于红黑树与链表之间转化的说明,翻译自 JDK 官方文档
因为 TreeNode(红黑树)的大小约为链表节点的两倍,所以我们只有在一个拉链已经拉了足够节点的时候才会转为 tree(参考
TREEIFY_THRESHOLD
)。并且,当这个 hash 桶的节点因为移除或者扩容后 resize 数量变小的时候,我们会将树再转为拉链。如果一个用户的数据的 hashcode 值分布得很均匀的话,就会很少使用到红黑树。理想情况下,我们使用随机的 hashcode 值,加载因子为情况,尽管由于粒度调整会产生较大的方差,节点的分布频率仍然会服从参数为的泊松分布。链表的长度为 8 发生的概率仅有
泊松分布,是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在 1838 年时提出。它会对随机事件的发生次数进行建模,适用于涉及计算在给定的时间段、距离、面积等范围内发生随机事件的次数的应用情形(来自维基百科)
泊松分布公式如下,
P
表示概率,N
表示某种函数关系,t
表示时间,n
表示数量。
关于 LoadFactor 默认值 0.75 的说明
参考链接:
由于哈希表的物理结构限制,存在以下的问题:
数组的容量过小,更容易出现哈希冲突
数组的容量过大,空间利用率不高
为了解决以上提出的两个问题,我们引入了loadFactor
来控制 hash 表的扩容操作
但是同样的,loadFactor
也有相对的值大小问题
loadFactor
越小,填满所需的数据就越少,哈希冲突的几率减少,但浪费了空间,而且还会提高扩容的触发几率,扩容相对于哈希表其他操作来说极度消耗系统性能。loadFactor
越大,填满所需的数据就越多,空间利用率提高,但发生哈希冲突的几率就变大了。
这就必须在"哈希冲突"与"空间利用率"两者之间有所取舍,尽量保持平衡
数学证明如下
前提:hash 值计算服从二项分布
假设插入数据没有倾向性,为全随机数据,则插入时数据的概率分布服从二项分布,具体理由如下:
实验的结果只有两种状态
向 hash 表内添加数据只有冲突与不冲突两种情况
每次进行 hash 值计算的时候概率相互独立
插入 hashmap 的 hash 值是随机的,并且他们经过 hash 运算都会映射到 hash 表的长度的地址空间上,那么这个结果也是随机的。所以,每次 put 的时候的概率是相互独立的
每次执行 hash 计算,成功的概率都是一样的
对于一组完全无序的数据,执行完 hash 值函数,hash 值发生冲突的理论概率是相等的
则根据二项分布的定义,可以认定在数据随机的情况下,可以认为 hash 值的分布是一个二项分布
对于一次元素插入,一般意义上我们需要追求插入发生哈希碰撞的概率尽可能的低
n 次事件里面,碰撞为 0 的概率,由二项分布公式得:
这个概率值需要大于 0.5,我们认为这样的 hashmap 可以提供很低的碰撞率。所以:
这时候,我们对于该式需求:当长度为 s 的时候,n 为多少次就应该进行扩容了?
又因为,所以推导如下:
所以可以得到
,其中 有
即问题转换为求函数极限问题,令,则转化为
再令 ,则有:
所以,最佳理论加载因子的值为:
考虑到 HashMap 的容量有一个要求:它必须是的次幂。当加载因子选择了就可以保证它与容量的乘积为整数。
1 | 16*0.75=12 |
除了 以外, 之间且向靠近的,满足条件的数还有 、可选,从中位数的角度,挑 比较完美。
另外,根据维基百科上数学证明,拉链法的加载因子最好限制在 以下,因为加载因子一旦超过 ,查表时的 CPU 缓存不命中(cache missing)会按照指数曲线上升。
综上, 是个比较完美的选择。