一、哈希表概述
1.1 核心思想
哈希表通过哈希函数将键映射到数组索引,实现平均 ( O(1) ) 的查找、插入和删除。
键(key) → 哈希函数 → 索引(index) → 数组存储 |
1.2 理想情况
[ \text{index} = hash(key) % \text{array.length} ]
二、哈希函数
2.1 好的哈希函数标准
- 确定性:相同输入得到相同输出
- 均匀性:输出均匀分布,减少冲突
- 高效性:计算速度快
2.2 Java String的hashCode
public int hashCode() { |
2.3 哈希值转索引
// Java 8之前 |
三、哈希冲突
3.1 什么是冲突
不同键通过哈希函数得到相同索引:
[ hash(k_1) % m = hash(k_2) % m ]
3.2 冲突解决方法
链地址法(Separate Chaining)
每个桶是一个链表,冲突元素链入链表。
索引: [0] [1] [2] |
优点:
- 实现简单
- 容量可以无限增长
缺点:
- 冲突严重时退化为链表 ( O(n) )
开放寻址法(Open Addressing)
冲突时探测下一个可用位置。
线性探测:
[ h(k, i) = (hash(k) + i) % m ]
二次探测:
[ h(k, i) = (hash(k) + c_1 i + c_2 i^2) % m ]
双重哈希:
[ h(k, i) = (hash_1(k) + i \cdot hash_2(k)) % m ]
优点:
- 缓存友好(数据连续)
缺点:
- 删除复杂(需要标记删除)
- 聚集问题
四、Java HashMap实现
4.1 核心字段
transient Node<K,V>[] table; // 哈希桶数组 |
4.2 节点结构
static class Node<K,V> implements Map.Entry<K,V> { |
4.3 put方法流程
- 计算key的hash值
- 定位桶位置:
(n - 1) & hash - 桶为空:直接插入
- 桶非空:遍历链表/红黑树
- key已存在:覆盖value
- key不存在:尾插法插入
- 检查是否需要扩容
4.4 扩容机制
final Node<K,V>[] resize() { |
扩容优化:
- 容量为2的幂,新索引只可能是原索引或
原索引+oldCap - 通过
(hash & oldCap) == 0判断
4.5 链表转红黑树
static final int TREEIFY_THRESHOLD = 8; // 链表长度≥8转红黑树 |
转换条件:
- 链表长度 ≥ 8
- 数组长度 ≥ 64(否则优先扩容)
五、负载因子(Load Factor)
5.1 定义
[ \text{负载因子} = \frac{\text{元素个数}}{\text{桶数量}} ]
5.2 默认值0.75的含义
- 空间与时间的平衡
- 过高:冲突增加,查询变慢
- 过低:空间浪费
5.3 调整策略
- 内存敏感:降低负载因子
- 速度敏感:保持或适当提高
六、ConcurrentHashMap
6.1 JDK 1.7 - 分段锁
Segment[16] |
6.2 JDK 1.8 - CAS + synchronized
transient volatile Node<K,V>[] table; |
锁粒度:只锁链表/红黑树的头节点。
七、总结
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(synchronized) | 是(CAS+锁) |
| 允许null | key/value | 否 | 否 |
| 扩容 | 2倍 | 2倍+1 | 2倍 |
| 遍历 | fail-fast | fail-fast | 弱一致性 |
哈希表的设计精髓在于哈希函数与冲突处理的平衡。理解HashMap的实现细节,对编写高效代码和排查问题都有重要帮助。