本文针对哈希表做全面梳理,如有纰漏还望指正
一、什么是哈希表
哈希表存储内容是<key,value>,意思就是你要存储的值(value)要给他设计一个标签方便查询。
举个例子:
你要去图书馆找书,你想看《笑话大全》你会把所有地方的书全遍历一遍吗?显然不会,你首先会去找文学区休闲类,如果首字母是‘x’的书都存在4号书架,你就直接去4号书架找就非常方便了。
而且现在有智能图书,你在手机app输入笑话大全(key)app会直接告诉你位置(value)
二、哈希表长啥样
哈希表可以是数组,索引代表key

也可以是是哈希桶(链式结构)后续说明

三、哈希冲突
对于数组哈希,我们插入元素一般用“取余法”(哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小)比如长为10的数组,我们存11就存到下标1位置,42就存下标为2位置
但是:52呢,也要存到下标为2位置咋办,之前已经存42了
这就是哈希冲突
四、解决冲突
4.1闭散列
4.1.1线性探测
在一个大小为10的数组,哈希函数采用取余法
一开始来个5

然后来个15,也是下标为5的位置,冲突了这时“找空位”

来25呢

这样你想找25,就从5开始往后找
缺点
• 一次聚集: 这是线性探测最致命的问题。随着插入的元素增多,连续占用的桶会形成一个越来越长的“聚集区”或“簇”。任何映射到这个聚集区头部的新元素,都需要沿着这个簇一步步向后探测,最终被添加到簇的尾部,使得簇变得更大。这会导致:
◦ 插入和查找的平均时间显著增加,接近 O(n)。
◦ 聚集区外的空闲位置很少被利用。
就比如数组全是5,15,25.······,这时候你要插入6,那不是得找老半天了
4.1.2二次探测
如果一次探测采用(hash(key) + i) % M,i探测次数,M数组长度
二次可以是(hash(key) + i^2) % M


这样避免一次聚集问题,但是也会有二次聚集
4.2开散列/哈希桶
刚才也提到哈希表底层可以是单链表,链表发生冲突我们可以这样:
公式为余5

4.2.1冲突转换
如果链表太长,查找依旧不方便
Java 8+中,如果链表长度达到一个阈值,会把单链表转换为红黑树提高查询效率
4.3冲突避免
避免冲突就是降低冲突率,首先你哈希函数要设计的简单些
其次是负载因子
负载因子值=哈希表数据个数/数组大小
当负载因子大于阈值会进行数组扩容(JDK1.8默认负载因子是0.75)
注意:扩容后需对原数据进行重新组合之前数据15挂在5上(大小10),大小20就得挂15上了。
五、JDK版本HashMap变化
-
数据结构
JDK 1.7:只有数组 + 链表,链表过长时查找效率低(O(n))
JDK 1.8:引入红黑树优化,链表长度超过阈值(8)且数组长度 ≥ 64 时转为红黑树,查找效率提升到 O(log n) -
插入方式
JDK 1.7(头插法):
新节点插入链表头部
优点:插入快(无需遍历)
缺点:并发扩容时可能形成环形链表,导致死循环
JDK 1.8(尾插法):
新节点插入链表尾部
优点:避免环形链表,但并发下仍可能数据覆盖 -
扩容迁移
JDK 1.7:遍历每个元素,重新计算哈希值和新下标,全部重排
JDK 1.8:利用数组长度是 2 的幂特性,判断新增 bit 位是 0 或 1:
为 0 → 留在原位置
为 1 → 移动到原位置 + 旧容量
避免重新计算哈希,效率更高 -
哈希算法
JDK 1.7:9次扰动处理(4次位运算 + 5次异或)
JDK 1.8:简化到 1次扰动(1次异或 + 1次无符号右移),兼顾效率与散列性 -
线程安全
JDK 1.7:头插法导致并发扩容时可能形成环形链表,出现死循环
JDK 1.8:尾插法避免死循环,但并发下仍存在数据覆盖、可见性等问题
六、HashMap的put底层逻辑
流程图:

-
计算哈希值(确定数据指纹)
• 调用 hash(key) 方法:
◦ 如果 key == null,哈希值直接定为 0
◦ 否则,取 key.hashCode(),并将其高16位与低16位进行异或运算 (h = key.hashCode()) ^ (h >>> 16)
• 目的:让哈希值的高位也能参与后续运算,减少冲突 -
计算数组下标(定位桶位置)
• 公式:i = (n - 1) & hash
◦ n 是当前数组长度(必须是 2 的幂)
◦ 该运算等价于 hash % n,但性能更高
• 结果:得到当前 key 应该落在数组的哪个位置(哪个桶) -
检查数组是否需要初始化或扩容
• 如果当前数组为 null 或长度为 0,先执行 resize() 扩容(初次 put 时会扩容为默认大小 16) -
定位到桶,分情况处理
情况 1:桶为空(无冲突)
• 直接在该位置新建一个普通节点(Node)并放入情况 2:桶不为空(发生哈希冲突)
• 进入该位置的链表或红黑树,开始查找查找逻辑:
• 比较第一个节点:
◦ 如果哈希值相同且 key 相等(equals 为 true),直接覆盖 value,返回旧值
• 如果不是第一个节点:
◦ 如果是树节点:调用红黑树的插入方法 putTreeVal
◦ 如果是链表节点:遍历链表,逐个比较 key
▪ 找到相同 key → 覆盖,返回旧值
▪ 没找到相同 key → 在链表尾部插入新节点 -
插入后检查:链表是否过长
• 如果新节点是插入到链表中,插入后判断链表长度是否 ≥ 8
• 如果达到阈值 8:
◦ 调用 treeifyBin() 尝试将链表转为红黑树
◦ 但转换前还有一个前置条件:数组长度是否小于 64
▪ 如果数组长度 < 64:优先进行扩容(不转树)
▪ 如果数组长度 ≥ 64:链表转为红黑树 -
最后检查:总元素个数是否超过阈值
• 每次插入结束后,HashMap 的 size 会加 1
• 判断 size > threshold(阈值 = 当前容量 × 负载因子 0.75)
• 如果超过阈值:触发扩容 resize()
◦ 容量扩大为原来的 2 倍
◦ 所有元素重新分布(rehash)
七、HashMap和Hashtable核心区别
7.1 线程安全对比:
- Hashtable:线程安全
- HashMap:非线程安全
7.2 null 值处理:
- HashMap:允许 null 作为 key,且以 null 作为 key 时,总是存储在 table 数组的第一个节点上(下标 0)
- Hashtable:不允许 null 作为 key
7.3 默认容量:
- HashMap:16
- Hashtable:11
7.4 哈希值计算方式:
- Hashtable:直接使用 key 的 hashCode()对 table 数组长度取模
- HashMap:对 key 的 hashCode() 进行二次哈希(扰动处理),得到更好的散列性,再对 table 数组长度取模

7270

被折叠的 条评论
为什么被折叠?



