哈希表全面讲解

本文针对哈希表做全面梳理,如有纰漏还望指正

一、什么是哈希表

哈希表存储内容是<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变化

  1. 数据结构
    JDK 1.7:只有数组 + 链表,链表过长时查找效率低(O(n))
    JDK 1.8:引入红黑树优化,链表长度超过阈值(8)且数组长度 ≥ 64 时转为红黑树,查找效率提升到 O(log n)

  2. 插入方式
    JDK 1.7(头插法):
    新节点插入链表头部
    优点:插入快(无需遍历)
    缺点:并发扩容时可能形成环形链表,导致死循环
    JDK 1.8(尾插法):
    新节点插入链表尾部
    优点:避免环形链表,但并发下仍可能数据覆盖

  3. 扩容迁移
    JDK 1.7:遍历每个元素,重新计算哈希值和新下标,全部重排
    JDK 1.8:利用数组长度是 2 的幂特性,判断新增 bit 位是 0 或 1:
    为 0 → 留在原位置
    为 1 → 移动到原位置 + 旧容量
    避免重新计算哈希,效率更高

  4. 哈希算法
    JDK 1.7:9次扰动处理(4次位运算 + 5次异或)
    JDK 1.8:简化到 1次扰动(1次异或 + 1次无符号右移),兼顾效率与散列性

  5. 线程安全
    JDK 1.7:头插法导致并发扩容时可能形成环形链表,出现死循环
    JDK 1.8:尾插法避免死循环,但并发下仍存在数据覆盖、可见性等问题

六、HashMap的put底层逻辑

流程图:
在这里插入图片描述

  1. 计算哈希值(确定数据指纹)
    • 调用 hash(key) 方法:
    ◦ 如果 key == null,哈希值直接定为 0
    ◦ 否则,取 key.hashCode(),并将其高16位与低16位进行异或运算 (h = key.hashCode()) ^ (h >>> 16)
    • 目的:让哈希值的高位也能参与后续运算,减少冲突

  2. 计算数组下标(定位桶位置)
    • 公式:i = (n - 1) & hash
    ◦ n 是当前数组长度(必须是 2 的幂)
    ◦ 该运算等价于 hash % n,但性能更高
    • 结果:得到当前 key 应该落在数组的哪个位置(哪个桶)

  3. 检查数组是否需要初始化或扩容
    • 如果当前数组为 null 或长度为 0,先执行 resize() 扩容(初次 put 时会扩容为默认大小 16)

  4. 定位到桶,分情况处理
    情况 1:桶为空(无冲突)
    • 直接在该位置新建一个普通节点(Node)并放入

    情况 2:桶不为空(发生哈希冲突)
    • 进入该位置的链表或红黑树,开始查找

    查找逻辑:
    • 比较第一个节点:
    ◦ 如果哈希值相同且 key 相等(equals 为 true),直接覆盖 value,返回旧值
    • 如果不是第一个节点:
    ◦ 如果是树节点:调用红黑树的插入方法 putTreeVal
    ◦ 如果是链表节点:遍历链表,逐个比较 key
    ▪ 找到相同 key → 覆盖,返回旧值
    ▪ 没找到相同 key → 在链表尾部插入新节点

  5. 插入后检查:链表是否过长
    • 如果新节点是插入到链表中,插入后判断链表长度是否 ≥ 8
    • 如果达到阈值 8:
    ◦ 调用 treeifyBin() 尝试将链表转为红黑树
    ◦ 但转换前还有一个前置条件:数组长度是否小于 64
    ▪ 如果数组长度 < 64:优先进行扩容(不转树)
    ▪ 如果数组长度 ≥ 64:链表转为红黑树

  6. 最后检查:总元素个数是否超过阈值
    • 每次插入结束后,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 数组长度取模
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值