HashMap三连问:数组布局、链表碰撞、树化升级,一次性讲透!

在Java面试中,HashMap几乎是“逢面必问”的经典题目。很多朋友一听到HashMap就紧张,担心被问到扩容、哈希冲突、红黑树这些概念。其实不用慌,哪怕你之前只是背过答案,只要理清三条主线问题,就能从容应对。

Q1:HashMap底层为什么是“数组+链表”?数组用来做什么?

HashMap的底层结构,可以想象成一个“带有编号的储物柜阵列”。这个阵列就是数组,每个位置叫做一个“桶”(bucket)。

当你往HashMap里存入一个键值对时,会先通过键的哈希值计算出一个数组下标,然后把这个键值对放到对应下标的桶里。

那问题来了: 不同的键有可能算出相同的数组下标,这就叫“哈希碰撞”。比如两个键的哈希值不同,但取模后都落到了第5号桶,该怎么办?

答案就是让每个桶里面不只是一个对象,而是一个链表。新来的键值对如果落在同一个桶,就追加到链表尾部(Java 8之前是头插法,后来改为尾插法,避免链表成环)。

所以结论很直观:

数组用来快速定位到具体的桶,时间复杂度O(1)。

链表用来解决同一个桶里的多个键值对,通过遍历链表逐个比较equals()找到正确的键。

用生活例子来理解:

数组好比一排信箱,每个信箱号就是哈希值的映射结果。两个不同的人(键)如果被分配到了同一个信箱号,就把他们的信件叠放在这个信箱里(链表)。查找时先根据信箱号定位,再在叠放的信件中逐一核对收件人名字。

Q2:链表什么时候会升级成红黑树?为什么要升级?

如果某个桶里的链表越来越长,查找一个元素的时间会变成O(n),性能就会下降。为了改善这种情况,Java 8引入了一个升级机制:当链表长度达到8,并且数组总长度达到64时,链表会转换为红黑树。

红黑树是一种自平衡的二叉查找树,查找、插入、删除的平均时间复杂度是O(log n)。相比链表的O(n),在数据量大的时候优势非常明显。

那为什么不是一上来就用红黑树? www.687game.com.cn

因为树节点占用内存大约是链表节点的两倍,对于元素很少的情况,链表更节省空间。所以HashMap设定了一个阈值:

链表长度小于等于6时,保持链表或者从树转回链表(树退化为链表)。

链表长度达到8且数组长度≥64时,转为红黑树。

如果链表长度达到8但数组长度小于64,不会转树,而是先对数组进行扩容,分散数据。

具体数字8和6之间有一个差值,是为了避免频繁转换造成性能抖动(就像空调不会在临界点反复开关)。

为什么要用红黑树而不是普通二叉查找树?

因为普通二叉查找树在最坏情况下会退化成一个长链表(比如插入的键刚好都是递增顺序),而红黑树通过颜色标记和旋转操作保证树大致平衡,避免性能崩塌。

Q3:HashMap扩容时,元素怎么重新分配?会不会特别慢?

HashMap的数组长度是有限的,当存放的元素越来越多,哈希碰撞的概率就会上升,所以需要动态扩容。

扩容触发条件:

当HashMap中存放的键值对数量超过了“数组长度 × 负载因子(默认0.75)”时,数组长度会翻倍(通常是变为原来的2倍)。

默认负载因子0.75是一个经验平衡值:太小会频繁扩容浪费空间,太大会增加哈希碰撞降低查询效率。

扩容的核心过程:

创建一个长度为原来2倍的新数组。

遍历旧数组的每个桶,把桶里的每个元素重新计算在新数组中的下标,并移动到新数组里。

对于链表节点,会拆分成两个小链表:一个留在原来的索引位置,另一个放到“原索引+旧数组长度”的位置。这个设计很巧妙,不需要为每个节点重新计算哈希值,只需要看哈希值新增的那一位是0还是1。

对于红黑树节点,同样会做拆分,如果拆分后树的节点数小于等于6,则退化为链表。

扩容会不会慢?

会。扩容需要遍历所有元素并重新分配,这是一个O(n)的操作。但HashMap的设计目标是:让扩容的次数尽量少(通过负载因子控制),并且单次扩容的耗时在可接受范围。在实际使用时,如果能提前预估数据量,可以在构造HashMap时指定初始容量,减少扩容带来的性能损耗。

进阶思考:HashMap为什么不线程安全?

这也是面试常见追问。HashMap没有对内部操作加锁,在多线程环境下同时进行put操作可能会导致:

链表形成循环依赖(虽然Java 8修复了头插法的成环问题,但仍存在数据覆盖等并发问题)。

两个线程同时触发扩容,可能导致部分元素丢失。

一个线程正在扩容,另一个线程来查询,可能查不到数据。

如果需要在多线程环境下使用,可以考虑:

Collections.synchronizedMap(new HashMap<>()),在每个方法上加了一个同步锁。

或者直接使用ConcurrentHashMap,它的分段锁或CAS机制效率更高。

但单纯面试角度,能说清楚HashMap为什么不安全,并且知道代替方案就够了。

常见误区澄清

误区一:哈希值就是数组下标。

不对。实际步骤是:调用键的hashCode()得到哈希值,然后HashMap再对哈希值做一个高位扰动运算(让哈希值的高位也参与低位运算),最后用扰动后的结果与数组长度-1做&运算得到下标。这样可以让哈希分布更均匀,减少碰撞。

误区二:负载因子越小越好。

不是。负载因子太小意味着数组比较空就会扩容,浪费内存空间,而且扩容本身也有性能开销。0.75是经过实践检验的折中值。

误区三:红黑树总是比链表快。

不成立。对于少量元素(比如几个或十几个),链表的内存局部性好,简单遍历甚至比树结构的节点跳转更快。所以HashMap才会在节点很少时把树退化为链表。

数组负责快速定位。

链表负责处理少量碰撞 www.haomir.com.cn/yxgl/350.html

当链表长度达到8且数组长度≥64,链表转红黑树,提高查询效率。

红黑树的查找、插入、删除时间复杂度为O(log n),链表是O(n)。

扩容时重新分配元素,尽量不影响整体性能。

结语

回过头来看,HashMap的设计其实很优雅:用数组解决定位速度,用链表解决哈希冲突,用红黑树解决极端碰撞下的性能退化。这三个结构的组合,正好对应了我们拆解出来的三个核心问题。

面试中遇到HashMap,不要慌。你只要从“数组+链表”讲起,接着说链表转红黑树的条件和原因,最后解释扩容机制和线程安全问题,基本就能覆盖80%以上的考点。剩下的细节(比如扰动函数、扩容时的位运算优化、树化与退化阈值)可以作为加分点,展示你的理解深度。

最重要的是,不要死记硬背。把HashMap想象成一个灵活变通的仓库管理员:货架多时用编号直接取(数组),货架拥堵时在格子里加一条小链子(链表),链子太长就换成带索引的格子树(红黑树)。这样理解后,无论面试官怎么追问,你都能有条理地答出来。

祝你面试顺利,HashMap三问轻松过关!

#我的求职进度条##秋招投递攻略##拿到offer之后,可以做些什么##你觉得mentor喜欢什么样的实习生##你的mentor是什么样的人?#
全部评论

相关推荐

本人是25届应届生,通过第三次实训营拿了实习offer后,一直干到转正,现在在牛子上班差不多一年了。很久没来过牛客了,原本想来发内推的,看了看全是骂公司的还是算罢,我原本无意替公司辩解,因为骂公司的基本上都没拿过公司offer的心生不满的,现在计算机校招行情非常差,大家有怨气很正常。但是,我发现多数人对七牛这套校招模式存在严重误解,很多人说什么“骗项目”,不是哥们,公司还是有200多号研发的,真没必要骗那几个校招生vibe出来的项目。我感觉往届校招生没人来说这事,那我就来说说吧。至于为啥我要来说呢,因为公司氛围其实还是比较好的,当你融入到公司的集体,你会自觉帮公司去干一些事情,很多时候都是这样。所以今天就想聊聊我自己,与七牛。顺便解释下七牛这套校招模式。2024.6,我孤身来到上海,主要是江南的书看多了,被骗了。我在高中时期就无比向往上海。提起上海,我会想起《上海堡垒》甜美爱情故事,也会想起《龙族》里面prada,lv那个纸醉金迷的上海,还会迷恋于《罗曼蒂克消亡史》那个血腥,却又柔情似水的上海。于是我投简历就对准上海周边投递,我太想来上海了。诚然,我收到了一家普陀公司的offer,主要是干前端。收到offer那一刻,我高兴的整晚的睡不着,我终于能奔赴我所迷恋的上海。但是那段时间却是我对上海最怯媚的时刻。刚来上海的时候为了省钱,愣是吃面条吃了三个多月,每周末奖励一顿麦当劳,每次吃麦当劳总感觉爽上了天,从此麦当劳成了我最喜欢吃的。还记得自己不太会做饭,挂面和冷藏鸡翅蔬菜放下去一锅炖,结果腥的根本没法吃,但还是硬着头皮吃下去了。后来,学会了烧鸡翅,就天天做,吃腻了又省吃俭用买了口空气炸锅,天天用新奥尔良粉腌鸡翅吃。每天起早贪黑,7点多起床出门挤二号线,九点多回来,在小区楼下买点冰冻鸡翅回去做饭十点,然后就睡觉了,一天也就赚200,还要应付秋招,面对冷面孔的面试官,被字节面试官嘲讽(从此恨透字节不用字节任何产品)。2024年,刚来上海的时候真的很难很难。我想起校招的时候我还是会忍不住哭,因为太难了。面对PUA你的领导,恶言嘲讽的面试官,还有为了省钱过日子的憋屈,还有要求你干这干哪的学校。那段时间我投遍了多数大厂,中厂,小厂,我希望能找到一份正式工作,然而实在是太难了。2024.11,我像日常一样投递到了七牛,hr告诉我面试要打比赛,那时候我求offer如饥似渴,早已顾不得那么多,便如饥似渴答应了,找上了我的高中同学,和一个搞算法的研究生朋友,便一头扎进了七牛的比赛。什么时候开赛我已经不记得了,但那时候ai还没有现在那么强,还是手写代码的时代,七牛给了两个星期给我们准备项目。我白天忙需求,晚上回去敲代码敲到12点,每天拖着疲惫的身体去上班。2024.11,我非常忐忑把作品提交了上去2024.12&nbsp;&nbsp;我接到七牛hr打电话告诉我让我去面试。我请了一天的假,还记得面试官问我,你觉得前端这份工作一般是做什么的,我一时难以言说,只能说对接下接口,实现页面。他说这是本职工作,我那时候也不知道怎么回答,于是我只能说还有优化下页面,他说你觉得前端就是干这些的?哈哈大笑。后来hr告诉我那是前端部门负责人,原本我非常忐忑不安,但是前端负责人还是给我过了面试。于是我拿到了七牛实习offer。进实训营了。不可否认的是,实训营本身其实也是校招一环吧,因为大家需要为了完成好那个项目,从产品架构设计出发,到项目一步步落地。尽管过了一年,我还是怀念实训营那段时光,我遇到了一群很好的人,我们一起吃饭,一起讨论一天的设计,一起过生日。我还记得,大家在公司二楼会议室,点外卖点了个蛋糕,一起唱生日歌的时光。完全不像牛客说的什么公司啥啥啥的,因为我的实训营体感非常好,我确实学到了很多东西,也学会了自己主动去做一些东西(因为以前我都是公司派什么活我就干什么),还收获了一群朋友。只不过后面因为大家要么去考研要么去别的公司,还有两个队友才大二无法经常来线下实习,于是最后只有我一个人留下来了。最后一天,我看着我和最后一个队友走出萨莉亚,他要去浦东机场,我要回唐镇。那么一刻我有点伤感,实训营这三个月我们一起加班加点赶项目,一起窝在会议室里面。如今结束了。望着队友离去的背影,三个月往事点点滴滴浮现在脑海,这是我来上海第二次想哭的时候。第一次是因为校招,第二次是因为实训营结束。然后就是实习,答辩转正,试用,试用答辩转正。我觉得很多人只是不想走完公司校招流程,和公司一点毛关系都没有。我参加七牛云比赛的时候,我感觉七牛反而是简单的,因为我很喜欢写代码,但是不擅长做那些恶心人的行测还有上来给你一道leetcode&nbsp;hard题目的面试。我完全没想过那个比赛会是所谓“骗项目”,我感觉这不就是很正常校招流程吗?只不过把笔试换成了比赛,但是随着多数人使劲黑公司,这个已经完全变味了,变成了“骗项目”。而且并非是什么没有offer,我就是完整走完流程的人,很多人都是自己离开公司,主要是钱的问题。
牛客35070997...:接好运
我的求职进度条
点赞 评论 收藏
分享
05-07 15:08
已编辑
长沙理工大学 Java
2年多经验,面Java,简历筛选到约面试极快,下午5点推进简历,6点通知晚上7点面试,线上面试30分钟拉满,给了反问环节。感受:比常见的初级面试难出一个档次,问题很多很密,一个答不上来或者漏了,立马抛出下一个问题。面试压力挺大的。面试官技术栈深,喜欢追问细节,就是不确定是我撑过了30分钟还是他凑时长。真题复盘:项目类1、你做过难度最大,最有成就感的事情是什么(答了我简历写上千万级数据迁移)2、&nbsp;为什么MySQL迁MongoDB?答错(MongoDB不适合说关联查询慢)3、三读一写怎么定的?压测数据:单读3600ms/单写1300ms,测了2读1写还慢,最后定3读1写4、迁移过程怎么确保不重复的?(答了游标分页规避边界遗漏,失败精准裁剪重试、断点重试、凌晨迁移)5、如果要做增量迁移,怎么处理?(只答了双写,没记住具体的做法)6、数据迁移的校验机制是什么样的,怎么验证数据没有丢失重复?(答的最大业务id和数据条数比对,因为我的断点续传机制可以保证没丢没重,当时也没出问题)7、优惠券小程序的业务流程是什么?(按照实际流程答了)8、优惠券怎么防止超领?是否有上线?(答的因为并发不大,直接数据库SQL原子更新)9、为什么要使用随机字符串做防重复?只用时间戳为什么不可以?答错(答的防重作用,时间戳作用。正确应该还说两个人同时登录可能时间戳完全一样)10、先验后调方案的落地是什么样的?(答得整个验签流程)11、这个验签是在拦截器做的吗?(应该是想问我拦截器那怎么写,但我当时做的时候是接口层弄得&nbsp;)Java基础1、基本数据类型?漏了byte2、那为什么还需要包装类型?没答好(只说了泛型必须对象,成员变量基本类型,方法参数包装类型)3、包装类和基本数据类型使用场景大概是哪些?没答好(还是和第二问说的一样的)4、&nbsp;从底层说说double金额隐患?(说了精度丢失,没展开IEEE&nbsp;754)6、Java中String为什么不可变(漏了类内部不提供修改方法)7、多线程实现方式。(连续几个没答对,太紧张,背了两种就卡壳被打断了)8、讲一下对死锁的理解。(说了死锁四大条件和一种解决方式)9、多线程中start和run方法的区别(说的run方法存放线程具体逻辑,start方法触发线程就绪状态,没背八股,自己推测的)10、ArrayList和LinkedList的底层在增加数据有什么不同?(前者需要扩容,中间慢,尾部快,后者中间慢头尾快)11、jdk8的新特性你了解哪些?没背八股(说的接口default和static、还有hashmap的变化,偏了但是面试官耐心听我说完了)12、自定义异常类是怎么做的?(写过也完全忘了)13、SpringBoot默认集成的Web容器?(Tomcat)14、怎么修改集成的容器?(不会)15、Redis数据库一致性的保障措施?(先更后删、延迟双删、binlog日志监听)总结:项目亮点(迁移数据+压测调优)顶住了,但Java基础和安全设计被扒了一层皮。30分钟撑下来了,但知道自己短板在哪。接下来对着错题一个一个啃。建议:java基础要背一些关于底层的东西,项目问的也不少,深挖5个问题,需要顶住。
查看13道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务