hashmap containsvalue时间复杂度_Java 面试者的内心独白:能不能不要再问我 HashMap 的问题?...

5aaaa945aaff816b923a9425f6f5b3cb.png

在 Java 基础面试中,HashMap 似乎是每个面试官必问的一道题。

一、阿里开发手册中对 HashMap 的描述

集合初始化时,指定集合初始值大小。

正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。

反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容 量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能

577688c55c133976aa9a57994c07f5da.png

如果你看到这,思考下:

为什么需要放置 1024 个元素,需要 7 次 扩容???

二、HashMap 的数据结构

hashmap 1.7 是 数组 + 单向链表

hashmap 1.8 是 数组 + 单向链表 + 红黑树

以下以 hashmap 1.8 说明

1、 基本属性

689d70f547484a2dd7eab5e4d9d42ed1.png

特别注意:

除了常说的默认容量 16,负载因子 0.75,由于加入了红黑树,当链表转为红黑树需要满足 2 个必要条件:

① 链表长度到8

15a629b7dd23ff1f4b6b84ad686690c3.png

② 一个是数组长度到 64

132395406446aeb40af914ec883b71a0.png

简单说,当你的链表长度即使到 8 ,但是数组长度小于 64 时,赶紧去扩容吧

2、链表结构

链表在Java7 叫 Entry 在 Java8 中叫 Node。

我们从下图中可以看到链表结构是单链表。

红黑树的结构比较复杂,暂时忽略,有兴趣的老铁可自行研究,太伤脑细胞了

4b9f347d25cc191cd2a4877bcb177483.png
29e0010d9f367af687960a8b7126ed9f.png

特别注意:

Java 8 之前是使用头插法(认为后添加的元素会先被查询),在 Java 8 改为尾插法。

由于多线程并发导致的扩容下,头插法由于在并发的时候原来的顺序被另外一个线程 a 颠倒了,而被挂起线程 b 恢复后拿扩容前的节点和顺序继续完成第一次循环后,又遵循 a 线程扩容后的链表顺序重新排列链表中的顺序,最终形成了环。

三、HashMap 如何解决冲突???

在说道 hashmap 如何解决冲突之前,我们先区分下 capacity 和 size 的区别?

1、容量 ( capacity)

比如我们的默认容量是 16,指的是数组的长度,而 size 指的是我们 put 的 key-value 个数

b611f4abd27fb524c82abfdd2fee79dd.png

输出结果:

capacity : 16

size : 1

问题:当我设置默认程度为 10,HashMap 的实际容量是 10 还是 16?

答案:16。在 tableSizeFor 的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的 2 的整数次幂的数

bc2155837468ffb053105fe6605a5039.png

2、hash 函数

2adccd807a5294b5fc533127afd72787.png

Java 8 之前 hash 算法 return h & (length-1);

默认容量选择 16 是为了 hash 算法平均分配到数组上

3、扩容

当 size 的个数 达到 (容量*负载因子)的值时,进行扩容。

ea779fdba6373cebdc7af0784778086b.png

四、HashMap 的时间复杂度???

Java 8 HashMap由数组+链表+红黑树组成的,数组是主体,链表是解决冲突,理想情况,不出现 hash 冲突,查找和增加的时间复杂度是 O(1);如果出现 hash 冲突,查找和增加的时间复杂度是 O(n)。

五、HashMap 线程不安全体现在哪些地方?

1、Java 7 中的 HashMap

Java 7 HashMap 链表上的数据是按“头插法”,“头插法”可能导致死循环和数据丢失现象;

9656ceb4f3c5b5ee9c952dcc4c20d7c2.png

transfer 函数

① “头插法” 死循环

请花几分钟浏览图中的代码

fb5942bb109516ac69d87e7ea9fbf274.png

假设未扩容前是这个样子

7d250562f36fc008e33d3690729e8072.png

假设单线程下,扩容后:

2a68a53cc934d4a9f0f83e8e2c537c1a.png

单线程下没啥问题


假设有两个线程 A 和 B

f3beaf175a2b2f71e1af3e1e34678dfd.png

线程 A 出现下列情况

be0d3b04b05a8e96dfde4d3a4ad00986.png

线程 A 挂起,线程 B 开始,完成 resize 操作

2a68a53cc934d4a9f0f83e8e2c537c1a.png

到这步,应该不少老铁看出问题了。线程 B k2 的下个节点是 k1,刚才挂起的线程 A 准备 hash k2,由于线程 B 的影响,会导致又回到 重新 hash k1,从而导致死循环。

2、Java 8 中的 HashMap

Java 8 中的 HashMap 使用了“尾插法”,避免了死循环问题。

当 线程 A 和线程 B 都执行到红框处代码时,

如果两条不同数据的 hash 值相同,

并且该位置为 null,

会出现数据覆盖的情况。

5da85925fc09748a55b629c6f1b4300c.png

@Python大星 | 文

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值