面试官:我们来说一说HashMap?

此时正值金三银四风骚跳槽季,小编也没有忍住想(加钱)提升技术的心理,默默的面试了多家大厂,包括美团、滴滴、百度、网易、腾讯。小编准备将大厂面试官问的最多的问题给大家分享出来,和大家一起学习进步。

一、先说一说你可能知道的

我默默地相信大家可能对Hashmap都已经很熟悉了,并且基本天天都能在撸Bug的时候用到这个容器,也在某些大神的口中,或者千篇一律的博客中知道了HashMap的底层原理是 数组+链表,JDK1.8后为改为 数组+链表+红黑树,因为1.8之前的版本现在已经很少用了,所以我们这里就不做具体分析了,我们这里只对1.8及以后版本的HashMap进行分析,那么HashMap什么时候会进行树化,又在什么时候会退化回链表呢?

  • 链表树化:当链表中出现大量Hash冲突时,链表数据就会过长,导致查询效率下降,此时我们的dog李大神绝对不能容忍这种低效率的查询行为,于是大神想到了用红黑树这种结构去存储出现大量冲突后的数据,这样在遍历冲突后的数据,时间复杂度可以从原先的O(n)提升到O(logn),非常牛逼,但是用红黑树就意味着一个节点所占用的存储空间将是原先的两倍,所以我们只有在其链表长度大于等于8(遵循泊松分布) 时 并且 hash表(table数组)大于等于64时才进行树化。
  • 树退化为链表:退化为链表的条件是,红黑树的节点小于等于6时红黑树会退化为链表。这个阈值为什么不是8或者是7呢,是为了避免树化与退化的阈值太过接近,导致在阈值附近的数据经常发生数据结构变化,影响性能。
    在这里插入图片描述
    在这里插入图片描述
二、再说说你有可能不知道的
  1. 我们平时在用hashmap的时候有没有想过,为什么它的负载因子要定为0.75呢?可能大家会说定在0.75之下的话,会导致经常扩容,浪费空间,定在0.75以上的话会导致大量的hash冲突。那为啥0.75最好呢?其实也0.75也并非最好,只是时间与空间的一种折中选择罢了。当然我们这里也不深究具体的原理,因为即便死磕具体原理对我们目前工作也没有太大意义。所以我们大概了解一下就好

在这里我可以直接告诉大家,这个0.75是根据牛顿二项式验算得来的。有兴趣的可以自己了解一下。以下内容多来自一个stackoverflow上的大神,因为小编在大学时高数也不是总100分,所以以下内容可能会引起不适,18岁以下人员禁止观看。勿喷。为表示诚意,附上大神原文链接:https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap/31401836#31401836

当我们假设数组的每一个下标对应的位置中,有数据和没有数据的概率分别为50%。

令s代表数组大小,n代表增加的key的数量。使用牛顿二项式定理,存储桶为空的概率为:
P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)
因此,如果少于:
log(2)/log(s/(s - 1)) keys
随着s达到无穷大,并且如果添加的键数使得P(0)= .5,则n / s迅速接近log(2):
lim (log(2)/log(s/(s - 1)))/s as s -> infinity = log(2) ~ 0.693...
  1. 为什么HashMap的容量要为2的整数次幂。

我们是不是很久很久之前就知道,hashmap在初始化的时候,不管你是将大小设置为多少,最后都会转化为大于或等于这个数的2的整数次幂。这到底是为什么?

我们在给当前key在table数组中定位一个下标时,**正常是要用这个key进行hash,然后用这个Hash后的值模当前数组的长度。但是道格李没有这么做,效率太低。他是这么做的。(见下图)
在这里插入图片描述

他用了当前的数组长度n-1再’与’上key的hash值。

大家在这里完全可以假设一下如果n不是2的整数次幂的情况。

如果n为一个奇数的话,比如11. 我们令n-1。最后等于10,我们知道10的二进制表达方式是 1010。我们可以看出偶数的二进制最后一位永远都是0,所以不管与上什么,最后一位都用不了,白白浪费了一位的空间。

但是当n为2的指数次幂时,比如8。8的二进制表达是 1000. 减去1之后是 0111。这样当0111与上hash之后就有可能生成0-7的任意数,确保当前数组中每一个空间都有可能被利用。

看到这里可能很多同学要问,除了这个就没有其它的地方用了这个2的整数次幂的设计吗?
当然有。

在我们jdk7中,每次进行扩容时,会将老数组中的数据转移到新数组中,这个时候会调用其rehash()方法,对每一个key在新数组中进行重新定位。这样非常低效,我们的道格李大神在jdk8中将其做了一些优化。

在jdk1.8中,hashmap在扩容时,没有将原数据中的数据重新rehash()。而是定义了四个指针变量,两个高位指针,两个低位指针。

 Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }

然后会直接用原先key的hash值与上原先的数组容量(e.hash & oldCap)。得出来的结果,如果是0就将其赋值给低位指针,要是非0(也就是原容量值)就赋值给高位指针(这里就是用到了我们2的指数次幂的特性,当2的指数次幂与任何二进制数进行与操作时,只有两种结果,要么是0要么是其本身)。
然后将低位指针指向的元素,直接挪到新数组的原位置(原来在老数组中的下标对应的位置),高位指针对应的对象挪到 原位置+原容量。这样就完全绕开了rehash操作。

以上就是我们本次要分享的hashmap的几处比较重要的知识点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云下牧羊人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值