HashMap 的理解
HashMap 的实现思路
即将key-value 存在一个列表中,然后根据key 的计算出的hash值(散列值)作为这个其在列表中的index。
这样就可以通过 list[index] 直接读取这个key-value 了,而不需要列表所有元素。
这里有一个问题就是,如果多个key 计算出来的hash 值一样,那么就存在位置重复的问题。 像这样,不同的数据拥有相同散列值的情况,被称为“冲突”。 解决的方法一般有链地址法(chaining)和开放地址法 (open addressing)两种。
链地址法是将拥有相同散列值的元素存放在链表中,因此随着元素个数的增加,散列冲突和查询链表的时间也跟着增加,就造成了性能的损失。 不过作为优化,当链表很长可以转为 红黑树,改善查询效率。
开放地址法则是在遇到冲突时,再继续寻找一个新的数据存放空间(一般称为槽)。寻找空闲槽最简单的方法,就是按顺序遍历,直到找到空闲槽为止。
JAVA 中是采用 链地址法 解决冲突的。
无论如何,当冲突对查找效率产生的不利影响超过某一程度时,就会对表的大小进行修改,从而努力在平均水平上保持 O(1) 的查找效率。
public class MyHashMap<K, V> {
private static class Node<K, V> {
K key;
V value;
Node<K, V> next;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private Node<K, V>[] table;
private int capacity = 16;
private int size = 0;
private final float loadFactor = 0.75f;
@SuppressWarnings("unchecked")
public MyHashMap() {
table = (Node<K, V>[]) new Node[capacity];
}
private int hash(K key) {
return (key == null) ? 0 : key.hashCode() & (capacity - 1);
}
public void put(K key, V value) {
if (size >= capacity * loadFactor) {
resize();
}
int index = hash(key);
Node<K, V> newNode = new Node<>(key, value);
if (table[index] == null) {
table[index] = newNode;
} else {
Node<K, V> current = table[index];
Node<K, V> prev = null;
while (current != null) {
if (current.key.equals(key)) {
current.value = value; // 更新
return;
}
prev = current;
current = current.next;
}
// 未找到 key,插入到链表末尾
prev.next = newNode;
}
size++;
}
public V get(K key) {
int index = hash(key);
Node<K, V> current = table[index];
while (current != null) {
if (current.key.equals(key)) {
return current.value;
}
current = current.next;
}
return null;
}
private void resize() {
int oldCapacity = capacity;
// 检查是否达到最大容量
if (oldCapacity >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 计算新容量
int newCapacity = oldCapacity << 1; // 双倍扩容
capacity = newCapacity;
// 创建新表
Node<K, V>[] newTable = (Node<K, V>[]) new Node[newCapacity];
// 重新哈希所有节点
for (int i = 0; i < oldCapacity; i++) {
Node<K, V> node = table[i];
if (node == null) continue;
// 拆分为两个链表
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
do {
Node<K, V> next = node.next;
// 判断节点应该留在原位置还是移动到新位置
if ((hash(node.key) & oldCapacity) == 0) {
if (loTail == null)
loHead = node;
else
loTail.next = node;
loTail = node;
} else {
if (hiTail == null)
hiHead = node;
else
hiTail.next = node;
hiTail = node;
}
node = next;
} while (node != null);
// 将链表放入新表
if (loTail != null) {
loTail.next = null;
newTable[i] = loHead; // 原位置
}
if (hiTail != null) {
hiTail.next = null;
newTable[i + oldCapacity] = hiHead; // 新位置
}
}
table = newTable;
}
}
Q&A
为什么计算key索引时还需要用hash值执行 & (capacity - 1)?
这个是因为hash 值很大,需要取模, 如 int index = hash(key) % capacity;。
不过 & (capacity - 1) 的作用和 % capacity 在 capacity 是 2 的幂时是等价的,而且&运算更快。
假设 capacity = 16(10000 二进制),则:
hash(key) % 16
= hash(key) & (16 - 1)
= hash(key) & 0b1111
效果:只保留 hash(key) 的低 4 位,相当于取模 16。
为什么 HashMap 的容量必须是 2 的幂?
-
&运算替代%, 提高计算速度。 -
减少哈希冲突
如果容量不是 2 的幂,hash % capacity 可能导致某些位从未被使用,增加冲突概率。
- 扩容优化
16 -> 32 扩容的索引计算对比
旧索引:hash & (oldCap - 1) = hash & 0b01111(取低 4 位)。
新索引:hash & (newCap - 1) = hash & 0b11111(取低 5 位)。
关键发现:
新索引的二进制比旧索引 多 1 位(第 5 位)。
因此:
如果 hash 的第 5 位是 0,新索引 = 旧索引。
如果 hash 的第 5 位是 1,新索引 = 旧索引 + oldCap。
实现如下
if ((e.hash & oldCap) == 0) {
// 保持原索引
newTab[j] = e;
} else {
// 新索引 = 原索引 + oldCap
newTab[j + oldCap] = e;
}
这样的优点有
-
可以直接复用 hash 值,无需重新调用 hashCode()。
-
位运算代替取模
-
均匀分布:
扩容后,原桶中的元素会 均匀分散到新桶(一半留在原位置,一半移动到 oldCap + 原位置)。
43万+

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



