一,知识串烧
1,哈希桶的核心结构:数组加链表(数组负责快速定位,链表负责解决哈希冲突);
2,节点设计:封装key,value,和next指针,支持链表式存储
3,插入流程:(put方法)计算下标➡️检查重复key➡️插入/更新节点
4,核心知识:通过哈希运算实现O(1)级别的查找/插入效率,用链表地址解决哈希冲突
二、完整代码实现(Java 版,可直接运行)
java
import java.util.Objects;
// 哈希桶核心类(存储key-value键值对)
class HashBucket<K, V> {
// 1. 内部节点类:封装key、value和下一个节点的引用
static class Node<K, V> {
K key; // 键(唯一)
V value; // 值
Node<K, V> next; // 指向下一个节点的指针
// 节点构造方法:创建节点时初始化key和value
public Node(K key, V value) {
this.key = key;
this.value = value;
this.next = null; // 初始化为空,后续通过next关联其他节点
}
}
// 2. 哈希桶成员变量
private Node<K, V>[] array; // 核心结构:数组(每个元素是链表的头节点)
private int usedSize; // 记录当前有效元素个数(不含冲突节点的重复计数)
private static final int DEFAULT_CAPACITY = 10; // 数组默认初始容量
// 3. 构造方法:初始化哈希桶(创建数组)
@SuppressWarnings("unchecked") // 抑制泛型数组转换警告
public HashBucket() {
this.array = new Node[DEFAULT_CAPACITY]; // 初始化数组,默认容量10
this.usedSize = 0;
}
// 4. 核心方法:插入/更新元素(put操作)
public void put(K key, V value) {
// 步骤1:计算当前key对应的数组下标(哈希运算核心)
int index = hash(key);
// 步骤2:检查该下标对应的链表中是否存在重复key
Node<K, V> cur = array[index]; // 从链表头节点开始遍历
while (cur != null) {
// 若key已存在:更新value(保证key唯一性)
if (Objects.equals(cur.key, key)) { // 用Objects.equals避免空指针
cur.value = value;
return; // 更新完成,直接返回
}
cur = cur.next; // 未找到重复key,继续遍历下一个节点
}
// 步骤3:不存在重复key,插入新节点(头插法,效率更高)
Node<K, V> newNode = new Node<>(key, value);
newNode.next = array[index]; // 新节点的next指向原链表头
array[index] = newNode; // 数组该位置的头节点更新为新节点
usedSize++; // 有效元素个数+1
}
// 辅助方法:计算key对应的数组下标(哈希函数)
private int hash(K key) {
if (key == null) {
return 0; // 若key为null,默认存放在下标0位置
}
// 核心逻辑:key的哈希值 % 数组长度(会议中"k除以数组长度"实际为取模,保证下标在数组范围内)
// 哈希值可能为负,用& Integer.MAX_VALUE转为正数
return (key.hashCode() & Integer.MAX_VALUE) % array.length;
}
// 辅助方法:获取元素(用于测试验证)
public V get(K key) {
int index = hash(key);
Node<K, V> cur = array[index];
while (cur != null) {
if (Objects.equals(cur.key, key)) {
return cur.value;
}
cur = cur.next;
}
return null; // 未找到返回null
}
// 辅助方法:获取有效元素个数(用于测试验证)
public int getUsedSize() {
return usedSize;
}
// 测试主方法
public static void main(String[] args) {
HashBucket<String, Integer> hashBucket = new HashBucket<>();
// 插入元素(包含重复key测试)
hashBucket.put("语文", 90);
hashBucket.put("数学", 95);
hashBucket.put("英语", 88);
hashBucket.put("数学", 98); // 重复key,更新value
// 验证结果
System.out.println("有效元素个数:" + hashBucket.getUsedSize()); // 输出3
System.out.println("数学成绩:" + hashBucket.get("数学")); // 输出98(已更新)
System.out.println("英语成绩:" + hashBucket.get("英语")); // 输出88
}
}
三、重点讲解(必须掌握)
1. 哈希桶结构设计重点
- 数组 + 链表的组合原因 :
- 数组:支持通过下标快速定位(时间复杂度 O (1)),是哈希桶高效的核心;
- 链表:解决「哈希冲突」(不同 key 计算出同一下标),无需移动元素,只需在链表末尾 / 头部插入。
- 节点类设计 :
- 必须包含
key(唯一标识)、value(存储数据)、next(链表关联); - 用泛型
<K, V>实现通用性,支持任意引用类型的 key 和 value。
- 必须包含
2. 插入流程(put 方法)重点
- 下标计算(hash 方法) :
- 核心公式:
(key的哈希值 & 正数掩码) % 数组长度; - 为什么不用直接除法?会议中 "k 除以数组长度" 实际是简化表述,取模(%)才能保证下标在
[0, 数组长度-1]范围内,避免数组越界; - 处理 null key:单独判断,默认存放在下标 0,符合 HashMap 等框架的设计逻辑。
- 核心公式:
- 重复 key 处理 :
- 遍历对应下标链表,用
Objects.equals(key1, key2)比较(避免空指针,且支持自定义 key 的 equals 重写); - 若存在重复 key,直接更新 value(而非新增节点),保证 key 的唯一性。
- 遍历对应下标链表,用
- 节点插入方式 :
- 采用「头插法」:新节点先指向原链表头,再更新数组的头节点引用;
- 优势:无需遍历到链表末尾,插入效率 O (1)(尾插法需遍历链表,效率 O (n))。
3. 核心变量作用
usedSize:仅记录有效元素个数(重复 key 更新时不递增),为后续「扩容机制」提供依据(如负载因子达到阈值时扩容);DEFAULT_CAPACITY:数组默认容量,可根据实际需求调整(通常设为 2 的幂,优化哈希运算)。
四、难点解析(易混淆 / 出错点)
1. 哈希冲突的理解与解决
- 为什么会冲突?:不同 key 的哈希值经过取模后,可能得到相同的数组下标(如 key1 哈希值 15,key2 哈希值 25,数组长度 10,均得到下标 5);
- 链地址法的优势:相比 "开放地址法"(如线性探测),链地址法不会导致元素扎堆,且插入 / 删除时无需移动其他元素,实现简单;
- 易错点:忘记处理冲突,直接覆盖数组原有节点,导致数据丢失。
2. 链表遍历与边界处理
- 空链表插入 :当数组下标对应的节点为
null(该位置无元素),直接将新节点作为头节点存入数组; - 非空链表遍历 :必须遍历到
cur == null才能确认无重复 key,否则可能遗漏中间节点的重复判断; - 易错点 :遍历链表时未判断
cur.next == null,导致空指针异常。
3. key 的比较逻辑
- 默认 equals 的局限性 :若 key 是自定义对象(如 User、Student),未重写
equals()和hashCode(),会默认比较对象地址,导致重复 key 无法被识别; - 解决方法 :自定义 key 类必须重写
equals()(判断内容相等)和hashCode()(保证内容相等的对象哈希值相同),否则哈希桶无法正常工作。
4. 哈希函数的优化
- 原始哈希值的问题 :key 的
hashCode()可能为负数,直接取模会得到负下标,导致数组越界; - 解决方案 :用
key.hashCode() & Integer.MAX_VALUE将哈希值转为正数(Integer.MAX_VALUE 是 0x7fffffff,二进制最高位为 0,与运算后最高位变为 0,即正数)。
五、了解部分(扩展知识)
1. 扩容机制
- 触发条件 :当
usedSize / 数组长度 ≥ 负载因子(默认负载因子 0.75),数组扩容为原来的 2 倍(通常设为 2 的幂,优化哈希取模运算); - 扩容流程:创建新数组→遍历原数组所有链表→重新计算每个节点的新下标→插入新数组(该过程称为 rehash);
- 作用:减少哈希冲突,保证哈希桶的查询 / 插入效率(负载因子过高会导致链表过长,效率退化到 O (n))。
2. 其他冲突解决方式
- 开放地址法:冲突时,按一定规则(如线性探测、二次探测)寻找数组中的下一个空位置;
- 哈希再哈希法:冲突时,使用第二个哈希函数重新计算下标;
- 公共溢出区法:将所有冲突的元素存入单独的溢出链表。
3. 实际应用场景
- 哈希桶是
HashMap(Java)、dict(Python)、unordered_map(C++)等容器的底层核心结构; - 适用于需要快速查找、插入、删除的场景(如缓存、数据库索引、用户信息存储等)。
4. 进阶优化
- 红黑树转换:当链表长度超过阈值(如 HashMap 中默认 8),将链表转为红黑树,将查询效率从 O (n) 优化到 O (log n);
- 扰动函数 :对 key 的哈希值进行多次异或和移位运算,减少哈希冲突(如 HashMap 的
hash()方法)。
六, 扩容部分的代码
java
import java.util.Objects;
// 哈希桶核心类(支持自动扩容,解决链表过长导致的效率退化问题)
class HashBucket<K, V> {
// 内部节点类(不变)
static class Node<K, V> {
K key;
V value;
Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
this.next = null;
}
}
// 成员变量(新增负载因子常量)
private Node<K, V>[] array;
private int usedSize;
private static final int DEFAULT_CAPACITY = 10; // 默认初始容量
private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子(扩容阈值)
// 构造方法(不变)
@SuppressWarnings("unchecked")
public HashBucket() {
this.array = new Node[DEFAULT_CAPACITY];
this.usedSize = 0;
}
// 核心方法:插入/更新元素(新增扩容触发逻辑)
public void put(K key, V value) {
// 步骤1:计算下标
int index = hash(key);
// 步骤2:检查重复key,存在则更新
Node<K, V> cur = array[index];
while (cur != null) {
if (Objects.equals(cur.key, key)) {
cur.value = value;
return;
}
cur = cur.next;
}
// 步骤3:头插法插入新节点
Node<K, V> newNode = new Node<>(key, value);
newNode.next = array[index];
array[index] = newNode;
usedSize++;
// 步骤4:判断是否需要扩容(核心新增逻辑)
// 负载因子 = 有效元素个数 / 数组长度,达到阈值则扩容
if (loadFactor() >= DEFAULT_LOAD_FACTOR) {
resize(); // 扩容为原容量的2倍
}
}
// 辅助方法:计算负载因子(新增)
private float loadFactor() {
// 强制转为float,避免整数除法(如7/10=0,而非0.7)
return (float) usedSize / array.length;
}
// 核心扩容方法(新增)
@SuppressWarnings("unchecked")
private void resize() {
// 1. 创建新数组:容量为原数组的2倍(通常设为2的幂,优化哈希取模)
Node<K, V>[] newArray = new Node[array.length * 2];
// 2. 重新哈希(rehash):遍历原数组所有节点,迁移到新数组
for (int i = 0; i < array.length; i++) {
Node<K, V> cur = array[i]; // 原数组当前下标对应的链表头
// 遍历当前链表的所有节点
while (cur != null) {
// 关键:保存当前节点的下一个节点(避免迁移时链表断裂)
Node<K, V> nextNode = cur.next;
// 3. 计算当前节点在新数组中的新下标(基于新数组长度取模)
int newIndex = hash(cur.key, newArray.length);
// 4. 头插法将当前节点插入新数组对应的链表
cur.next = newArray[newIndex];
newArray[newIndex] = cur;
// 5. 移动到下一个节点继续迁移
cur = nextNode;
}
}
// 6. 替换原数组引用:新数组成为哈希桶的核心存储结构
this.array = newArray;
}
// 重载hash方法:支持传入自定义数组长度(用于扩容时计算新下标)
private int hash(K key, int newArrayLength) {
if (key == null) {
return 0;
}
// 基于新数组长度取模,保证下标在新数组范围内
return (key.hashCode() & Integer.MAX_VALUE) % newArrayLength;
}
// 原hash方法(用于普通插入/查询时计算下标)
private int hash(K key) {
return hash(key, array.length); // 复用重载方法,默认传入当前数组长度
}
// 辅助方法:获取元素(不变)
public V get(K key) {
int index = hash(key);
Node<K, V> cur = array[index];
while (cur != null) {
if (Objects.equals(cur.key, key)) {
return cur.value;
}
cur = cur.next;
}
return null;
}
// 辅助方法:获取有效元素个数(不变)
public int getUsedSize() {
return usedSize;
}
// 辅助方法:获取当前数组容量(新增,用于测试验证扩容结果)
public int getCurrentCapacity() {
return array.length;
}
// 测试主方法(新增扩容验证逻辑)
public static void main(String[] args) {
HashBucket<String, Integer> hashBucket = new HashBucket<>();
// 测试1:初始状态验证
System.out.println("初始容量:" + hashBucket.getCurrentCapacity()); // 输出10
System.out.println("初始负载因子:" + (float) hashBucket.getUsedSize() / hashBucket.getCurrentCapacity()); // 0.0
// 测试2:插入元素触发扩容(默认负载因子0.75,10*0.75=7,插入第8个元素时扩容)
hashBucket.put("语文", 90);
hashBucket.put("数学", 95);
hashBucket.put("英语", 88);
hashBucket.put("物理", 92);
hashBucket.put("化学", 89);
hashBucket.put("生物", 91);
hashBucket.put("历史", 85); // 第7个元素,负载因子7/10=0.7(未达阈值)
System.out.println("插入7个元素后容量:" + hashBucket.getCurrentCapacity()); // 仍为10
hashBucket.put("地理", 87); // 第8个元素,负载因子8/10=0.8(超过0.75,触发扩容)
System.out.println("插入8个元素后容量:" + hashBucket.getCurrentCapacity()); // 输出20(扩容为2倍)
// 测试3:验证扩容后元素可正常获取(rehash成功)
System.out.println("地理成绩:" + hashBucket.get("地理")); // 输出87
System.out.println("数学成绩:" + hashBucket.get("数学")); // 输出95
System.out.println("有效元素个数:" + hashBucket.getUsedSize()); // 输出8(无数据丢失)
// 测试4:扩容后继续插入,验证负载因子控制
hashBucket.put("政治", 86);
hashBucket.put("信息技术", 93);
System.out.println("扩容后插入2个元素,负载因子:" + (float) hashBucket.getUsedSize() / hashBucket.getCurrentCapacity()); // 10/20=0.5(未达阈值)
}
}