hashmap中用红黑树不用其他树_10个HashMap问题搞定面试官

18acb3b7358a7460e376458b8f5f6aac.png

废话不多说,看题:

1、说一下HashMap的数据结构?

JDK1.7使用的是数组+ 单链表的数据结构。
JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构。当阈值是默认阈值0.75,链表的深度大于等于8,数组容量大于等于64时,扩容的时候会把链表转成红黑树,时间复杂度从O(n)变成O(logN);当红黑树的节点深度小于等于6时,红黑树会转为链表结构。

2、简述下HashMap的工作原理?

JDK1.7使用的是数组+ 单链表的数据结构。
JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构。
HashMap 通过 put & get 方法存储和获取。

put() 方法存储对象:

①、调用 hash(key.hashCode()) 方法计算 key 的 hash 值(其中JDK1.7用了9次扰动处理=4次位运算+5次异或;JDK1.8只用了2次扰动处理=1次位运算+1次异或),然后和(数组长度-1)做异或运算,得出数组下标;

②、当table中的元素个数大于阈值(capacity * loadfactor)时,容器会进行扩容resize操作,将table数组大小扩充为2倍;

③、
i.如果key的hash值对应的table下标元素为空,说明还没有元素,则直接插入,若不为空,则说明存在元素,发生了hash冲突,接下来要遍历链表或红黑树进行匹配查找;

ii.如果遍历的过程中,==或equals返回true,说明找到了对应的对象,直接更新该对象的value,返回旧value;

iii. 如果遍历结束,==或equals还是返回false,说明没有找到了对应的对象,则需要在该链表或红黑树中插入(JDK1.7使用头插法,JDK1.8 使用尾插法);
注:JDK1.8当链表长度大于等于8时,会把链表转换成红黑树;当红黑树深度小于等于6时,会把红黑树转换成链表。

get() 方法获取对象:

①、和put方法第一步一样,通过调用hash(key.hashCode()) 方法,然后和(数组长度-1)做异或运算,得出数组下标;

②、顺序遍历链表或红黑树,==或equals返回true,则返回对应的value值,否则返回null。

3、如何解决hash冲突?

通过异或运算能够是的计算出来的hash比较均匀,不容易出现冲突。但是偏偏出现了冲突现象,这时候该如何去解决呢?

在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而hashMap中处理hash冲突的方法就是链地址法。

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

4、为什么HashMap中table数组用transient修饰?

transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。我们再回到源码中,考虑一个问题:桶数组 table 是 HashMap 底层重要的数据结构,不序列化的话,别人还怎么还原呢?

这里简单说明一下吧,HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject两个方法自定义了序列化的内容。这样做是有原因的,试问一句,HashMap 中存储的内容是什么?不用说,大家也知道是键值对。所以只要我们把键值对序列化了,我们就可以根据键值对数据重建 HashMap。有的朋友可能会想,序列化 table 不是可以一步到位,后面直接还原不就行了吗?这样一想,倒也是合理。但序列化 talbe 存在着两个问题:

1)table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间。
2)同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

以上两个问题中,第一个问题比较好理解,第二个问题解释一下。HashMap 的get/put/remove等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。

综上所述,大家应该能明白 HashMap 不序列化 table 的原因了,下面是HashMap自定义的序列化代码:

private void writeObject(java.io.ObjectOutputStream s)
    throws IOException {
    int buckets = capacity();
    // Write out the threshold, loadfactor, and any hidden stuff
    // 写入一些属性值,待反序列化时用到
    s.defaultWriteObject();
    s.writeInt(buckets);
    s.writeInt(size);
    internalWriteEntries(s);
}

 // Called only from writeObject, to ensure compatible ordering.
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
   Node<K,V>[] tab;
    if (size > 0 && (tab = table) != null) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
              //写入键值对
                s.writeObject(e.key);
                s.writeObject(e.value);
            }
        }
    }
}

private void readObject(java.io.ObjectInputStream s)
        throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    reinitialize();
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings);
    else if (mappings > 0) { // (if zero, use defaults)
        // Size the table using given load factor only if within
        // range of 0.25...4.0
        float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
        float fc = (float)mappings / lf + 1.0f;
        int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                   DEFAULT_INITIAL_CAPACITY :
                   (fc >= MAXIMUM_CAPACITY) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor((int)fc));
        float ft = (float)cap * lf;
        threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                     (int)ft : Integer.MAX_VALUE);
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
        table = tab;

        // Read the keys and values, and put the mappings in the HashMap
        // 读出键值对,放入hashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}

5、为什么JDK1.8中不一下子把整个链表变为红黑树呢?

为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释:

(1)put和remove过程中,红黑树要通过左旋,右旋、变色这些操作来保持平衡,另外构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

6、说一下红黑树?

由于二叉查找树(BST)存在数据倾斜的问题(极端情况下会形成一个链表),所以平衡二叉查找树(Balanced BST)产生了。平衡树在插入和删除的时候,会通过旋转操作将高度保持在logN。其中两款具有代表性的平衡树分别为AVL树和红黑树。AVL树由于实现比较复杂,而且插入和删除性能差,因此在实际环境中我们更多的是应用红黑树。

红黑树(Red-Black Tree)以下简称RBTree的实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等,是函数式语言中最常用的持久数据结构之一。
由于在java8里HashMap的底层实现用RBTree取代链表,使得性能得到了提升,因此很多人才去关注或者深入学习红黑树。

红黑树特性,RBT树上的每个节点,都要遵循下面的规则:
① 每个节点都是红色或者黑色;
② 根节点必须始终是黑色;
③ 没有两个相邻的红色节点;
④对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点。

RBT树在理论上还是一棵BST树,但是它在对BST的插入和删除操作时会维持树的平衡,即保证树的高度在[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。RBT的查找操作就是BST的查找操作。

7、为什么 ConcurrentHashMap 比 HashTable 效率要高?

HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap:
① JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
② JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。

8、HashMap 和 HashTable 有什么区别?

① HashMap 是线程不安全的,HashTable 是线程安全的;
② 由于线程安全,所以 HashTable 的效率比不上 HashMap;
③ HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
④ HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤ HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

9、HashMap,LinkedHashMap,TreeMap 有什么区别?

HashMap:一般情况下使用最多,输出的顺序和输入的顺序不相同,在 Map 中插入、删除和定位元素时使用;

TreeMap:实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器),在需要按自然顺序或自定义顺序遍历键的情况下使用;

LinkedHashMap:保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;在需要输出的顺序和输入的顺序相同的情况下使用。

10、说一下JDK1.7 ConcurrentHashMap的并发度?

concurrencyLevel不能代表ConccurentHashMap实际并发度,ConccurentHashMap会使用大于等于该值的2的幂指数的最小值作为实际并发度,实际并发度即为segments数组的长度。创建时未指定concurrencyLevel则默认为16。

并发度对ConccurentHashMap性能具有举足轻重的作用,如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

欢迎小伙伴们留言交流~~


原作者: DIGGKR
原文链接: https:// mp.weixin.qq.com/s/QPCA LbcJzJVH45ecXjM5MA
原出处:掘客DIGGKR
侵删

de01e9e50491f32b7195d4868e663d12.gif
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值