• 关于HashMap的一些思考


    一、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,仍旧在原位,因为它的的值大于数组的长度
    ------------------------------我是博客签名------------------------------
    座右铭:不要因为知识简单就忽略,不积跬步无以至千里。
    版权声明:自由转载-非商用-非衍生-保持署名。
    本作品采用知识共享署名 4.0 国际许可协议进行许可。
    ----------------------------------------------------------------------
  • 相关阅读:
    [leetcode] 135. Candy (hard)
    [leetcode] 134. Gas Station (medium)
    [OpenGL] 绘制并且判断凹凸多边形、自相交多边形。
    [leetcode] 45. Jump Game II(hard)
    [leetcode] 55. Jump Game (Medium)
    [leetcode] 392. Is Subsequence (Medium)
    [leetcode] 147. Insertion Sort List (Medium)
    [leetcode]914. X of a Kind in a Deck of Cards (easy)
    [leetcode] 234. Palindrome Linked List (easy)
    [leetcode] 290. Word Pattern (easy)
  • 原文地址:https://www.cnblogs.com/wuqinglong/p/14595599.html
Copyright © 2020-2023  润新知