zoukankan      html  css  js  c++  java
  • HashMap知识点

    1.基本数据结构

     

      1. JDK1.7 数组 + 链表

      2. JDK1.8 数组 + (链表 | 红黑树)

    2.树化与退化

      1.树化意义

        1.红黑树用来避免Dos攻击,防止链表过长时性能下降,树化应该是偶然情况,是保底策略.

        2.hash 表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n ),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表

        3.hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

      2.树化规则

        1.当链表长度超过树化阈值8时,先尝试扩容来减少链表长度,如果数组容量已经 >= 64,才会进行树化.

      3.退化规则

        1. 情况1:在扩容时如果拆分树时,树元素 <= 6 则会退化链表

        2. 情况2:在 remove 树节点时若 root 根节点,根节点的左右子节点或者孙节点只要有一个为 null 也会退化为链表.

    3.索引计算

      1.索引计算方法

        1.首先,计算对象的hashCode()

        2.再进行调用HashMap的hash()方法进行二次哈希 (二次哈希是为了综合高位数据,让哈希分布更为均匀)

        3.最后 哈希结果 & (数组容量 - 1) 得到索引位置

      2.数组容量为何是2的 n 次幂

        1.计算索引时,如果是2的n次幂可以使用位于运算替代取模,效率更高.

        2.扩容时 hash & 旧容量 == 0的元素留在原来位置,否则新位置 = 旧位置 + 旧容量

        3.上述都是为了配合容量为2的 n 次幂时的优化手段,例如 HashTable的容量就不是2的 n 次幂,并不能说哪种设计更好.

    4. put 与 扩容

      1. put 流程

        1.HashMap 是懒惰创建数组的,首次使用才创建数组

        2.计算索引(桶下标)

        3.如果桶下标还没人占用,创建 Node 占位返回

        4.如果桶下标已经有人占用

          1.已经是 TreeNode 走红黑树的添加或更新逻辑

          2.是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑

        5.返回前检查容量是否超过阈值,一旦超过进行扩容

      2. 1.7 与 1.8 的区别

        1. 表插入节点时,1.7是头插法,1.8是尾插法

        2. 1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容

        3. 1.8在扩容计算Node索引时会优化

      3. 扩容加载因子 (factor) 为何默认是0.75f

        1. 在空间占用和查询时间之间取得比较好的权衡

        2. 大于这个值,空间节省了,但是链表就会比较长,影响性能

        3. 小于这个值,冲突减少了,但是扩容更频繁,空间占用更多

    5.并发问题

      1.扩容死链 (1.7会存在)

        1.7 源码如下:

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    • e 和 next 都是局部变量,用来指向当前节点和下一个节点

    • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

       

      线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

      第一次循环

      • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b

      • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)

      • 当循环结束是 e 会指向 next 也就是 b 节点

      • 第二次循环

        • next 指向了节点 a

        • e 头插节点 b

        • 当循环结束时,e 指向 next 也就是节点 a

         

         

        第三次循环

        • next 指向了 null

        • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成

        • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

        •  

  • 相关阅读:
    面向对象与组合
    异常处理和三级菜单练习
    装饰器和生成器
    序列化模块
    leetcode_498. 对角线遍历
    leetcode_566. 重塑矩阵
    leetcode_59. 螺旋矩阵 II
    leetcode_54. 螺旋矩阵
    leetcode_396. 旋转函数
    leetcode_200. 岛屿数量
  • 原文地址:https://www.cnblogs.com/xuxiaobai13/p/15458083.html
Copyright © 2011-2022 走看看