HashMap 面试题整理

news/2024/10/11 9:24:49/

HashMap 面试题整理

介绍下 HashMap 的底层数据结构

在 JDK1.7 和 JDK1.8 中有所差别:

在 JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

在这里插入图片描述

在 JDK1.8 ,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(loogn),而链表是O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

  • 当链表长度大于等于 8 且数组长度大于等于 64 才会转为红黑树
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行扩容,而不是转换为红黑树,以减少搜索时间。

在这里插入图片描述

为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡;而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候,红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这时浪费性能的。

不用红黑树,用二叉树可以吗?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

当链表转为红黑树后,什么时候退化成链表?

当 6 的时候退化成链表。中间有个差值 7 可以防止链表和红黑树之间频繁地切换。假设一下,如果设计成链表个数超过 8 则链表转换成树结构,链表个数小于 8 则树结构转换成链表,如果一个 HashMap 不停的插入、删除元素,链表个数在 8 左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

为什么链表改为红黑树的阈值是 8 ?

是因为泊松分布,我们来看看作者在源码中的注释:

在这里插入图片描述

翻译过来大概的意思:理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

默认加载因子为什么是 0.75,而不是 0.6 或者 0.8 ?

回答这个问题前,我们先来看下 HashMap 的默认构造函数:

在这里插入图片描述

Node[] table 的初始化长度 length(默认值是 16),Load factor 为负载因子(默认值是 0.75),threshold 是 HashMap 所能容纳键值对的最大值。threshold = length * factor。

也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

默认的 loadFactor 是 0.75,0.75 是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下:

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子 Load factor 的值
  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子 loadFactor 的值,这个值可以大于 1.

我们来追溯性下坐着在源码中的注释(1.7):

在这里插入图片描述

翻译过来大概得意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查询成本(体现在大多数的 HashMap 类的操作,包括 get 和 put)。设置初始大小时,应该考虑预计的 entry 数在 map 及其负载系数,并且尽量减少 rehash 操作的次数。如果初始容量大于最大条目数除以负载因子,rehash 操作将不会发生。

HashMap 中 key 的存储索引是怎么计算的?

首先根据 key 的值计算出 hashcode 的值,然后根据 hashcode 计算出 hash 值,最后通过 hash & (length - 1) 计算得到存储的位置。看看源码的实现:

在这里插入图片描述

1.7 索引计算:

在这里插入图片描述

1.8 索引计算:

这里的 Hash 算法本质上就是三步:取 key 的 hashcode 值、根据 hashcode 计算出 hash 值、通过 & 计算下标。其中 JDK1.7 和 JDK1.8 的不同之处,就在于第二步。我们来看看详细的过程,以 JDK1.8 为例,n 为 table 的长度:

在这里插入图片描述

JDK1.8 为什么要 hashcode 异或其右移十六位的值?

因为在 JDK1.7 中扰动了多次,计算 hash 值的 性能会稍差一点点。从速度、功效、质量来考虑,JDK1.8 优化了高位运算的算法,通过 hashcode() 的高 16 位异或低 16 位实现:(h = k.hashCode() ^ (h >>> 16))。这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低 Bit 都参与到 Hash 的计算中,同时不会有太大的开销。

为什么 hash 值都要与 length -1 相与?

  • 把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快
  • 把 length 总是 2 的 n 次方时,h & (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。

HashMap 数组的长度为什么是 2 的幂次方

这样做效果上等同于取模,在速度、效率上比直接取模也要快得多。除此之外,2 的 N 次幂有助于减少碰撞几率。如果 length 为 2 的幂次方,则 length-1 转化为二进制必定是 1111… 的形式,在与 h 的二进制与操作效率会非常快,而且空间不浪费。我们举个例子,看看下图:

在这里插入图片描述

在这里插入图片描述

当 n=15 时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7 就会在一个位置形成链表,4 和 5 的结果也是一样,这样就会导致查询速度降低。

如果我们进一步分析,还会发现空间浪费非常大,以 length=15 为例,在 1、3、5、7、9、11、13、15 这八处没有存放数据。因为 hash 值在与 14(即 1110)进行 & 运算时,得到的结果最后一位永远都是 0,即 0001、0011、0101、0111、1001、1011、1101、1111 位置处是不可能存储数据的。

JDK1.7 和 1.8 的 put 方法去区别是什么?

区别在两处:

解决哈希冲突时,JDK1.7 只使用链表,JDK1.8 使用链表+红黑树,当满足一定条件,链表会转为红黑树

链表插入元素时,JDK1.7 使用头插法插入元素,在多线程的环境下有可能导致环形链表,扩容的时候会导致死循环。因此 JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了,但 JDK1.8 的 HashMap 仍然是线程不安全的。

HashMap 的扩容方式?

HashMap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。

那扩容的具体步骤是什么?让我们来看看源码 。

在这里插入图片描述

这里就是使用一个容量更大的数组来代替已有的容量小的数组,transfer() 方法将原有 Entry 数组的元素拷贝到新的 Entry 数组里。

在这里插入图片描述

newTable[i] 的引用赋给了 e.next ,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到 Entry 链的尾部(如果发生了 hash 冲突的话)。

扩容在 JDK1.8 中有什么不一样?

JDK1.8 做了两处优化:

  1. resize 之后,元素的位置在原来的位置,或者原来的位置 + oldCap(原来哈希表的长度)。不需要像 JDK1.7 的实现那样重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引 + oldCap”。这个设计非常的巧妙,省去了重新计算 hash 值的时间。

​ 如下图所示,n 为 table 的长度,图(a)表示扩容前的 key1 和 key2 两种 key 确定索引位置的实例,图(b)表示扩容后 key1 和 key2 两种 key 确定索引位置的示例,其中 hash1 是 key1 对应的哈希与高位运算结果。

在这里插入图片描述

元素再重新计算 hash 之后,因为 n 变为 2 倍,那么 n-1 的 mask 范围在高位多 1 bit(红色),因此新的 index 就会发生这样的变化:

在这里插入图片描述

  1. JDK1.7 中 rehash 的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置(头插法)。JDK1.8 不会倒置,使用尾插法。

下图为 16 扩充为 32 的 resize 示意图:

在这里插入图片描述

key 可以为 NULL 吗?

可以,key 为 NULL 的时候,hash 算法最后的值以 0 来计算,也就是放在数组的第一个位置。


http://www.ppmy.cn/news/1503256.html

相关文章

景区导览系统开发

景区导览系统的开发是一个综合性的项目,涉及多个领域的知识和技术,包括互联网、移动开发、数据库管理、地图导航、人工智能等。以下是一个详细的开发流程介绍: 一、需求分析 市场调研:了解当前旅游市场的趋势和游客的需求&#…

Java实现数据库图片上传(包含从数据库拿图片传递前端渲染)-图文详解

目录 1、前言: 2、数据库搭建 : 建表语句: 3、后端实现,将图片存储进数据库: 思想: 找到图片位置(如下图操作) 图片转为Fileinputstream流的工具类(可直接copy&#…

Qt实现简易CAD软件的开发:技术解析与实现

文章目录 简易CAD软件的开发:技术解析与实现引言项目概述程序入口主窗口的实现主窗口类定义(mainwindow.h)主窗口类实现(mainwindow.cpp) 自定义绘图视图自定义绘图视图类定义(myqgraphicsview.h&#xff0…

【WEB】ctfshow-萌新-web9-15

文章目录 题目介绍&#xff1a;题目分析&#xff1a;payload&#xff1a; 题目介绍&#xff1a; ctfshow-萌新计划-web9-15 <?php # flag in config.php include("config.php"); if(isset($_GET[c])){$c $_GET[c];if(preg_match("/system|exec|highlight…

编程小白如何成为大神?大学新生的最佳入门攻略

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

PostgreSQL入门与进阶学习,体系化的SQL知识,完成终极目标高可用与容灾,性能优化与架构设计,以及安全策略

​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 文章目录 概述基础篇初级篇进阶篇…

高效办公新选择:2024年福昕PDF同类软件精选

现在大家都开始使用PDF文档格式来进行文件的传输&#xff0c;随着使用率的增加&#xff0c;大家已经不局限于“只读”式的PDF文件了&#xff0c;使用福昕pdf这种PDF编辑软件就可以体验阅读、编辑PDF。 1.福昕PDF编辑器 链接直达>>https://editor.foxitsoftware.cn 这…

基于Python的哔哩哔哩国产动画排行数据分析系统

需要本项目的可以私信博主&#xff0c;提供完整的部署、讲解、文档、代码服务 随着经济社会的快速发展&#xff0c;中国影视产业迎来了蓬勃发展的契机&#xff0c;其中动漫产业发展尤为突出。中国拥有古老而又璀璨的文明&#xff0c;仅仅从中提取一部分就足以催生出大量精彩的…