一、HashMap的负载因子的作用

当 HashMap 中的元素个数(包含链表、红黑树上的元素)达到数组长度的0.75倍的时候,开始扩容。

二、HashMap的负载因子为什么是0.75

主要是为了提高空间利用率和减少查询成本(也可以说是尽可能减少hash冲突)。

三、为什么槽位数必须使用2^n

如果想让 Hash 结果分布更加均匀,首先想到的就是使用取余(%)操作。重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash & (length - 1) 的前提是 length 是 2 的 n 次方)。” 并且采用二进制位操作 &,相对于 % 能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

四、解决Hash冲突的方法

1、开放地址法

公式:fi(key) = (f(key)+di) MOD m (di=0,1,2,3,......,m-1)

key:待放入数组(hash表)的元素;m:数组长度

当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。

(1)线性探测法

思想是:通过公式计算出元素在数组中的下标,如果下标上没有元素,直接放进去;如果下标中有元素,则公式中的 di 依次 +1 重新计算,直到查找到没有元素的下标。不然数组就满了,需要扩容。

(2)二次探测法

思想是:通过改变 di 的计算方式来查询没有元素的下标,具体计算方式就是 di=-12,12,-22,22,…,-(q * 10 + 2),(q * 10 + 2),q <=m / 2。至于这个 di 的取值我也没研究,摘抄过来的,但是这个探测法的思想得知道。

考虑的情况是,如果通过公式计算出来下标之后的所有下标都有元素占据了,而这个下标的前面的有空闲的,通过第一种方法可以算出来,但是计算的次数比较多,通过这个方法可以减少计算次数。

(3)伪随机数探测再散列

思想是:di 的值是通过随机函数得到的。如果随机函数的种子相同,那么得出来的 di 也相同,查询就ok了。

总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是我们常用的解决冲突的办法。

2、拉链法

就是当产生 Hash 冲突时,在冲突的节点上形成链表,HashMap 就是使用的拉链法解决的 Hash 冲突。

五、为什么链表长度达到 8 的时候就要转为红黑树了?

当使用 0.75 作为负载因子时,链表中的长度达到 8 几乎是不可能的,均衡策略吧。

引用 HashMap 源码中的注释:

* 0:    0.60653066* 1:    0.30326533* 2:    0.07581633* 3:    0.01263606* 4:    0.00157952* 5:    0.00015795* 6:    0.00001316* 7:    0.00000094* 8:    0.00000006* more: less than 1 in ten million

六、HashMap扩容时元素的位置发生了什么变化?

分为三种情况:

  • 对于数组上的元素:直接使用已经计算出来的hash值重新计算新下标放入新数组。
  • 对于链表:将一条链表拆分为两条,hash值大于数组长度的新链表放在新数组,小于的就放在原数组。
  • 对于红黑树:将数拆为两条链表,hash值大于数组长度的新链表放在新数组,小于的就放在原数组,最后,重新判断两条链表是否需要转为红黑树。

关键代码:

do {    next = e.next;    if ((e.hash & oldCap) == 0) {        if (loTail == null)            loHead = e;        else            loTail.next = e;        loTail = e;    }    else {        if (hiTail == null)            hiHead = e;        else            hiTail.next = e;        hiTail = e;    }} while ((e = next) != null);

例如:oldCap 是 16,那么扩容之后的新数组长度就是 32,链表上的元素分别是 7,23,39。(整数的hash值就是本身)

  7 :0000 0111& 16:0001 0000--------------- =  :0000 0000 # 0,仍旧在原位   17:0001 0001& 16:0001 0000--------------- =  :0001 0000 # 非0,需要放在 [17, 32) 之间   23:0001 0111& 16:0001 0000--------------- =  :0001 0000 # 非0,需要放在 [17, 32) 之间   39:0010 0111& 16:0001 0000--------------- =  :0000 0000 # 0,仍旧在原位,因为它的的值大于数组的长度



©著作权归作者所有:来自51CTO博客作者李季谦千的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 2021-03-29:无序数组arr,子数组-1和1的数量一样多,请问最长子数组的
  2. 前端技巧:遍历数组都有哪些方式呢?
  3. Ubuntu系统网络配置及shell脚本编程之函数数组等用法详解
  4. 串与数组,广义表
  5. C语言中的择中,二分查找算法解析
  6. C语言指针的理解
  7. 使用C语言判断密码是否正确,三次失败就退出,超详细教程!!
  8. python常用的图像处理工具有哪些?工具推荐!
  9. 零基础学习Java开发,这些学习笔记送给你

随机推荐

  1. android 使用xml定义自己的View
  2. android 页面布局时定义控件ID时@id/XX和
  3. Android Application Fundamentals——An
  4. Android中字体的处理
  5. Android Studio 配置快捷方式生成JNI头文
  6. android配置文件详解
  7. Android的BUG(四) - Android(安卓)app的卡
  8. Android数据的四种存储方式
  9. Android so减包相关
  10. Android锁屏控制