面试官为什么-HashMap-的加载因子是075

3次阅读

共计 4195 个字符,预计需要花费 11 分钟才能阅读完成。

有很多货色之前在学的时候没怎么留神,笔者也是在重温 HashMap 的时候发现有很多能够去细究的问题,最终是会回归于数学的,如 HashMap 的加载因子为什么是 0.75?

本文次要对以下内容进行介绍:

  • 为什么 HashMap 须要加载因子?
  • 解决抵触有什么办法?
  • 为什么加载因子肯定是 0.75?而不是 0.8,0.6?
    • 为什么 HashMap 须要加载因子?

      HashMap 的底层是哈希表,是存储键值对的构造类型,它须要通过肯定的计算才能够确定数据在哈希表中的存储地位:

      static final int hash(Object key) {int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} AbstractMappublic int hashCode() {     int h = 0;     Iterator<Entry<K,V>> i = entrySet().iterator();     while (i.hasNext())         h += i.next().hashCode();     return h;}

      个别的数据结构,不是查问快就是插入快,HashMap 就是一个插入慢、查问快的数据结构。

      但这种数据结构容易产生两种问题:① 如果空间利用率高,那么通过的哈希算法计算存储地位的时候,会发现很多存储地位曾经有数据了(哈希抵触);② 如果为了防止产生哈希抵触,增大数组容量,就会导致空间利用率不高。

      而加载因子就是示意 Hash 表中元素的填满水平。

      加载因子 = 填入表中的元素个数 散列表的长度

      加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;

      加载因子越小,填满的元素越少,抵触产生的机会减小,但空间节约了更多了,而且还会进步扩容 rehash 操作的次数。

      抵触的机会越大,阐明须要查找的数据还须要通过另一个路径查找,这样查找的老本就越高。因而,必须在“抵触的机会”与“空间利用率”之间,寻找一种均衡与折衷。

      所以咱们也能晓得,影响查找效率的因素次要有这几种:

      • 散列函数是否能够将哈希表中的数据平均地散列?
      • 怎么解决抵触?
      • 哈希表的加载因子怎么抉择?
        • 本文次要对后两个问题进行介绍。

          解决抵触有什么办法?

          1. 凋谢定址法

          Hi = (H(key) + di) MOD m,其中 i =1,2,…,k(k<=m-1)

          H(key)为哈希函数,m 为哈希表表长,di 为增量序列,i 为已发生冲突的次数。其中,凋谢定址法依据步长不同能够分为 3 种:

          1.1 线性探查法(Linear Probing):di = 1,2,3,…,m-1

          简略地说,就是以以后抵触地位为终点,步长为 1 循环查找,直到找到一个空的地位,如果循环完了都占不到地位,就阐明容器曾经满了。举个栗子,就像你在饭点去街上吃饭,挨家去看是否有地位一样。

          1.2 平方探测法(Quadratic Probing):di = ±12, ±22,±32,…,±k2(k≤m2)

          绝对于线性探查法,这就相当于的步长为 di = i2 来循环查找,直到找到空的地位。以下面那个例子来看,当初你不是挨家去看有没有地位了,而是拿手机算去第 i2 家店,而后去问这家店有没有地位。

          1.3 伪随机探测法:di = 伪随机数序列

          这个就是取随机数来作为步长。还是用下面的例子,这次就是齐全按情绪去选一家店问有没有地位了。

          但凋谢定址法有这些毛病:

          • 这种办法建设起来的哈希表,当抵触多的时候数据容易堆集在一起,这时候对查找不敌对;
          • 删除结点的时候不能简略将结点的空间置空,否则将截断在它填入散列表之后的同义词结点查找门路。因而如果要删除结点,只能在被删结点上增加删除标记,而不能真正删除结点;
          • 如果哈希表的空间曾经满了,还须要建设一个溢出表,来存入多进去的元素。
            • 2. 再哈希法

              Hi = RHi(key), 其中 i =1,2,…,k

              RHi()函数是不同于 H()的哈希函数,用于同义词产生地址抵触时,计算出另一个哈希函数地址,直到不发生冲突地位。这种办法不容易产生堆集,然而会减少计算工夫。

              所以再哈希法的毛病是:减少了计算工夫。

              3. 建设一个公共溢出区

              假如哈希函数的值域为 [0, m-1],设向量 HashTable[0,…,m-1] 为根本表,每个重量寄存一个记录,另外还设置了向量 OverTable[0,…,v]为溢出表。根本表中存储的是关键字的记录,一旦发生冲突,不论他们哈希函数失去的哈希地址是什么,都填入溢出表。

              但这个办法的毛病在于:查找抵触数据的时候,须要遍历溢出表能力失去数据。

              4. 链地址法(拉链法)

              将抵触地位的元素结构成链表。在增加数据的时候,如果哈希地址与哈希表上的元素抵触,就放在这个地位的链表上。

              拉链法的长处:

              • 解决抵触的形式简略,且无堆集景象,非同义词绝不会发生冲突,因而均匀查找长度较短;
              • 因为拉链法中各链表上的结点空间是动静申请的,所以它更适宜造表前无奈确定表长的状况;
              • 删除结点操作易于实现,只有简略地删除链表上的相应的结点即可。
                • 拉链法的毛病:须要额定的存储空间。

                  从 HashMap 的底层构造中咱们能够看到,HashMap 采纳是数组 + 链表红黑树的组合来作为底层构造,也就是凋谢地址法 + 链地址法的形式来实现 HashMap。

                  为什么 HashMap 加载因子肯定是 0.75?而不是 0.8,0.6?

                  从上文咱们晓得,HashMap 的底层其实也是哈希表(散列表),而解决抵触的形式是链地址法。HashMap 的初始容量大小默认是 16,为了缩小抵触产生的概率,当 HashMap 的数组长度达到一个临界值的时候,就会触发扩容,把所有元素 rehash 之后再放在扩容后的容器中,这是一个相当耗时的操作。

                  而这个临界值就是由加载因子和以后容器的容量大小来确定的:

                  临界值 = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR

                  即默认状况下是 16x0.75=12 时,就会触发扩容操作。

                  那么为什么抉择了 0.75 作为 HashMap 的加载因子呢?这个跟一个统计学里很重要的原理——泊松散布无关。

                  泊松散布是统计学和概率学常见的离散概率分布,实用于形容单位工夫内随机事件产生的次数的概率分布。有趣味的读者能够看看维基百科或者阮一峰老师的这篇文章:泊松散布和指数分布:10 分钟教程[1]

                  等号的右边,P 示意概率,N 示意某种函数关系,t 示意工夫,n 示意数量。等号的左边,λ 示意事件的频率。

                  在 HashMap 的源码中有这么一段正文:

                  * Ideally, under random hashCodes, the frequency of* nodes in bins follows a Poisson distribution* (http:en.wikipedia.orgwikiPoisson_distribution) with a* parameter of about 0.5 on average for the default resizing* threshold of 0.75, although with a large variance because of* resizing granularity. Ignoring variance, the expected* occurrences of list size k are (exp(-0.5) * pow(0.5, k) * factorial(k)). The first values are:* 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

                  在现实状况下,应用随机哈希码,在扩容阈值(加载因子)为 0.75 的状况下,节点呈现在频率在 Hash 桶(表)中遵循参数均匀为 0.5 的泊松散布。疏忽方差,即 X = λt,P(λt = k),其中 λt = 0.5 的状况,按公式:

                  计算结果如上述的列表所示,当一个 bin 中的链表长度达到 8 个元素的时候,概率为 0.00000006,简直是一个不可能事件。

                  所以咱们能够晓得,其实常数 0.5 是作为参数代入泊松散布来计算的,而加载因子 0.75 是作为一个条件,当 HashMap 长度为 lengthsize ≥ 0.75 时就扩容,在这个条件下,抵触后的拉链长度和概率后果为:

                  0:    0.606530661:    0.303265332:    0.075816333:    0.012636064:    0.001579525:    0.000157956:    0.000013167:    0.000000948:    0.00000006

                  那么为什么不能够是 0.8 或者 0.6 呢?

                  HashMap 中除了哈希算法之外,有两个参数影响了性能:初始容量和加载因子。初始容量是哈希表在创立时的容量,加载因子是哈希表在其容量主动扩容之前能够达到多满的一种度量。

                  在维基百科来形容加载因子:

                  对于凋谢定址法,加载因子是特地重要因素,应严格限度在 0.7-0.8 以下。超过 0.8,查表时的 CPU 缓存不命中(cache missing)依照指数曲线回升。因而,一些采纳凋谢定址法的 hash 库,如 Java 的零碎库限度了加载因子为 0.75,超过此值将 resize 散列表。

                  在设置初始容量时应该思考到映射中所需的条目数及其加载因子,以便最大限度地缩小扩容 rehash 操作次数,所以,个别在应用 HashMap 时倡议依据预估值设置初始容量,以便缩小扩容操作。

                  抉择 0.75 作为默认的加载因子,齐全是工夫和空间老本上寻求的一种折衷抉择。

                  想要理解更多 Java 架构技术 的,能够关注我一下,我后续也会整顿更多对于架构技术这一块的知识点分享进去,

                  外面会分享一些:Spring,MyBatis,Netty 源码剖析,高并发、高性能、分布式、微服务架构的原理,JVM 性能优化,并发编程这些成为架构师必备的常识体系。
                  还有大厂面试题,看了相对受益匪浅

正文完
 0