一、哈希表核心概念
1. 时间复杂度对比
- 哈希表:查询、增删时间复杂度 O(1)(常数级)// 核心优势,依靠哈希直接定位
- 普通数组/链表:查询时间复杂度 O(n) // 需要逐个遍历,数据量越大效率越低
2. 底层存储结构
哈希表底层采用 数组 + 链表 组合结构:
- 主体为节点数组,数组每个位置可存放K-V节点
- 存储形式:以键值对(K-V) 存储数据,数据统一封装为独立节点
- 核心流程:数据key → 计算hash值 → 映射数组下标 → 完成数据存储/查询
3. Hash算法与下标映射
- Hash算法规则
- 相同key经过哈希算法,必然得到相同hash值
- 不同key大概率得到不同hash值,存在哈希冲突(不同key算出相同hash值)// 哈希表必须解决的问题
- 下标计算规则
通过hash值 & 数组最大下标运算得到数组存储下标 // 位运算效率远高于取模运算 - 冲突分类
- 哈希冲突:不同key生成相同hash值
- 下标映射冲突:不同hash值,经位运算后得到同一个数组下标
4. 冲突解决方案:拉链法
也叫链地址法,是本案例采用的冲突解决方式:
- 数组对应下标位置存入第一个节点
- 后续冲突节点,挂载到前一个节点的
next引用上,形成单向链表 - 进阶优化:当链表长度达到 8 时,链表转换为红黑树(平衡二叉树) // 避免链表过长导致查询效率退化
5. 数组扩容机制
- 扩容触发条件:数组已使用容量达到总容量的 75%(负载因子0.75)
- 扩容规则:数组容量扩大为原长度2倍 ,数组长度始终保持
2^n// 保证位运算下标计算正常 - 扩容逻辑:扩容后所有原有节点需要重新计算下标,迁移至新数组
二、核心实体类:节点类 HNode
用于封装单个键值对数据,同时维护链表指针与hash值。
java
package com05.dc.zyf0531;
/**
* 哈希表节点类:封装K-V键值对,实现链表结构
* @param <K> 键类型
* @param <V> 值类型
*/
public class HNode<K, V> {
K key; // 键
V value; // 值
HNode<K, V> next; // 链表下一个节点 // 实现拉链法的核心引用
int hash; // 当前key对应的hash值 // 提前存储,避免重复计算
// 构造方法:初始化节点数据
public HNode(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
三、自定义哈希表实现类 ZHashMap
基于数组+链表实现简易HashMap,包含初始化、添加、扩容、查询核心方法。
java
package com05.dc.zyf0531;
import java.util.Random;
/**
* 自定义哈希表实现 底层:数组+单向链表
* @param <K> 键泛型
* @param <V> 值泛型
*/
public class ZHashMap<K, V> {
HNode<K, V>[] table; // 存储节点的底层数组 // 哈希表主体
int length; // 底层数组总长度
int size; // 哈希表中总元素个数
int useArrSize; // 数组中已被占用的下标位置数量
// 数组默认初始容量 16,遵循 2^n 规则
static final int DEFAULT_INITIAL_CAPACITY = 16;
int count; // 重复key计数
// 无参构造:初始化哈希表
public ZHashMap() {
length = DEFAULT_INITIAL_CAPACITY;
table = new HNode[length];
size = 0;
useArrSize = 0;
}
/**
* 根据key查询对应value
* @param key 查找的键
* @return 对应value
*/
public V get(K key) {
// 1. 计算hash值
int hash = key.hashCode();
// 2. 计算数组下标
int index = hash & (length - 1);
HNode<K, V> node = table[index];
// 遍历当前下标下的链表
while (node != null) {
// 先比hash,再比key,避免哈希冲突误判
if (node.hash == hash && (node.key == key || node.key.equals(key))) {
return node.value;
}
node = node.next;
}
return null; // 未找到返回空
}
/**
* 添加/修改键值对 核心方法
* @param key 键
* @param value 值
*/
public void put(K key, V value) {
// 1. 计算key的hash值
int hash = key.hashCode();
// 2. 位运算计算存储下标 hash & (数组长度-1)
int index = hash & (length - 1);
HNode<K, V> first = table[index];
// 情况1:当前下标无节点,直接存入新节点
if (first == null) {
HNode<K, V> node = new HNode<>(key, value, hash);
table[index] = node;
size++;
useArrSize++;
} else {
// 情况2:下标存在节点,遍历链表判断是否重复key
// 先判断头节点是否为重复key
if (first.hash == hash && (first.key == key || first.key.equals(key))) {
first.value = value; // 覆盖旧值
count++;
System.out.println("相同数据计数:" + count);
} else {
HNode<K, V> temp = first;
int flag = 0; // 标记是否找到重复key
// 遍历链表后续节点
while (temp.next != null) {
temp = temp.next;
if (temp.hash == hash && (temp.key == key || temp.key.equals(key))) {
temp.value = value;
count++;
System.out.println("相同数据计数:" + count);
flag = 1;
break;
}
}
// 链表遍历完毕,无重复key,追加新节点到链表尾部
if (flag == 0) {
temp.next = new HNode<>(key, value, hash);
size++;
}
}
}
// 判断是否满足扩容条件:已用容量 >= 总容量*0.75
if (length * 0.75 <= useArrSize) {
resize();
System.out.println("扩容 len:" + length);
}
}
/**
* 数组扩容方法:容量翻倍,重新迁移所有节点
*/
public void resize() {
// 位运算 <<1 等价于 *2,数组扩容2倍
int newLength = length << 1;
HNode<K, V>[] newTable = new HNode[newLength];
int newUseArrSize = 0;
int newSize = 0;
// 遍历原数组所有下标
for (int i = 0; i < length; i++) {
HNode<K, V> first = table[i];
if (first != null) {
// 遍历当前下标下的整条链表
while (first != null) {
// 记录原节点下一引用,防止断链
HNode<K, V> oldNext = first.next;
// 重新计算新数组下标
int nindex = first.hash & (newLength - 1);
HNode<K, V> temp = newTable[nindex];
if (temp == null) {
// 新下标无节点,直接放入
newTable[nindex] = new HNode<>(first.key, first.value, first.hash);
newUseArrSize++;
} else {
// 新下标存在节点,遍历到链表尾部追加
while (temp.next != null) {
temp = temp.next;
}
temp.next = new HNode<>(first.key, first.value, first.hash);
newSize++;
}
// 遍历原链表下一个节点
first = oldNext;
}
}
}
// 替换底层数组,更新属性
length = newLength;
useArrSize = newUseArrSize;
table = newTable;
size = newSize;
System.out.println("扩容后: 数组长度:" + length + " 数组使用长度:" + useArrSize + " 元素个数:" + size);
}
// 测试主方法
public static void main(String[] args) {
Random ran = new Random();
ZHashMap<String, Integer> map = new ZHashMap<>();
// 批量插入10万条数据测试
for (int i = 0; i < 100000; i++) {
map.put("hello" + i, i);
}
System.out.println(map.size);
System.out.println(map.useArrSize);
System.out.println(map.count);
}
}
四、关键流程总结
-
新增数据流程
计算key哈希值 → 位运算求数组下标 → 判断下标是否为空 → 空则直接存节点;非空则遍历链表 → 存在重复key则更新value,无则链表尾部追加节点 → 判断是否触发扩容。
-
扩容流程
负载因子达到0.75触发扩容 → 数组容量翻倍 → 遍历原数组及所有链表节点 → 每个节点重新计算下标并迁移至新数组 → 替换底层数组。
-
冲突处理流程
出现下标冲突时,使用拉链法构建链表;链表长度过长(≥8)会转为红黑树,保证查询效率。
五、重点知识点备注
- 下标计算使用
hash & (length - 1),仅当数组长度为2^n时运算等效于取模,效率更高。 - 判断key是否相同时,先比较hash值,再比较key ,减少
equals调用次数,提升效率。 - 负载因子0.75是时间与空间的平衡值:过小浪费空间,过大加剧哈希冲突。
- 扩容是耗时操作,会重新迁移所有数据,合理初始容量可减少扩容次数。