⚡⚡
🔥 个人主页:北极的代码(欢迎来访)
🎬 作者简介:java后端学习者
❄️ 个人专栏:LeetCode刷题日记.....持续更新中
✨ 命运的结局尽可永在,不屈的挑战却不可须臾或缺!
我们已经完成了对链表章节算法的学习,关于这些方法思路的总结,我准备找一个专门的时间总结出来,以思维导图 为发散,将知识点串联起来,接下来我们继续学习哈希表及其相关的算法题目,覆盖基础知识到实战演练。
(英文名字为Hash table,国内也有一些算法书籍翻译 为散列表,大家看到这两个名称知道都是指hash table就可以了)。 我学习的是java,这里分享的就是在java中的哈希表,跟c语言或者其他的语言差别挺大的,java的哈希表是已经封装好了的,不需要自己去实现,直接拿来用就可以。
要刷力扣 哈希表题型,Java 里哈希表核心就是 2 个类:HashMap + HashSet,我把所有刷题必需的基础知识、用法、区别、技巧一次性讲全。
哈希表 = 数组 + 哈希函数(本质定义) 类比一下: 哈希表 = 快递柜系统 ,数组 = 快递柜格子
哈希函数 = 按手机号分配格子(计算出哈希值) Java HashMap = 带挂袋和大箱子的快递柜
链表 = 格子放不下,挂个袋子串起来 红黑树 = 袋子太长,换成有序大箱子
哈希函数又是什么:
把任意对象,算出一个 int 类型的哈希值,用来定位数组下标的公式。
hashCode() 方法
> public native int hashCode();
给每个对象返回一个 int 数字
同一个对象多次调用,结果应该一样 想让两个对象视为同一个 key,必须 equals 为 true 且 hashCode 相同
HashMap 自己的哈希函数(真正用的)
HashMap 不会直接用 key.hashCode(),它会再做一次扰动,让分布更均匀,减少冲突。
JDK 源码里的哈希函数:
static final int hash(Object key) { int h; // key 为 null 时哈希值固定为 0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
h >>> 16:把哈希值右移 16 位
^:异或运算
目的:把高 16 位和低 16 位混合,让哈希值更散列,减少冲突。
最后一步:算出数组下标
有了哈希值,还要转成数组下标:
index = hash & (table.length - 1); AI写代码 等价于取模,但位运算更快。
补充:这里是怎么减少冲突的,通过一个例子理解
假设我们有两个字符串:
"Aa" "BB" 我们直接算它们的 hashCode 和 原始下标:
灾难发生了!如果没有扰动,"Aa" 和 "BB" 这两个完全不同的字符串,会被映射到同一个数组下标(下标 0)!它们会发生严重的哈希冲突,都挤在一个链表后面。
但扰动之后?扰动是把高位和低位异或,改变了低位信息,让它们落在不同位置,完美避开冲突。
总结:
无扰动计算:就是直接用 hashCode & (len-1)。
核心目的:
扰动不是为了改这次的结果,而是为了保证大量不同的字符串,低位信息也不同,从而最大限度避免哈希冲突。
所以,Java 必须加那个 hash() 扰动函数。
| 类 | 用途 | 底层 | 重复规则 |
|---|---|---|---|
HashMap<K,V> | 存键值对(统计次数、映射关系) | 数组 + 链表 + 红黑树 | key 不可重复,重复会覆盖 |
HashSet<E> | 只存值(去重、判断存在) | 基于 HashMap | 元素不可重复 |
HashMap 最全用法
HashMap 是力扣哈希题使用频率最高的工具,专门处理:
- 统计字符 / 数字出现次数
- 两数之和这类映射查找
- 记录索引位置
- 导包
import java.util.HashMap;
- 创建对象
// 常见写法(key 和 value 可任意类型) HashMap<Integer, Integer> map = new HashMap<>(); HashMap<String, Integer> map = new HashMap<>();
- 核心方法(背会这 10 个足够刷所有哈希题)
`// 1. 添加/修改元素:key 不存在=添加,存在=覆盖 map.put(key, value);
// 2. 获取元素:根据 key 拿 value map.get(key);
// 3. 判断是否包含某个 key map.containsKey(key);
// 4. 判断是否包含某个 value(很少用) map.containsValue(value);
// 5. 删除元素 map.remove(key);
// 6. 获取键值对数量 map.size();
// 7. 清空 map.clear();
// 8. 判断是否为空 map.isEmpty();
// 9. 获取所有 key(遍历用) map.keySet();
// 10. 获取所有 value(遍历用) map.values();`
- 遍历 HashMap(刷题必背 2 种)
for (Integer key : map.keySet()) { int value = map.get(key); System.out.println(key + " : " + value); }
for (Map.Entry<Integer, Integer> entry : map.entrySet()) { int key = entry.getKey(); int value = entry.getValue(); }
HashSet 最全用法
HashSet 用于:
- 判断一个元素是否出现过
- 去重
- 找重复元素
- 导包
import java.util.HashSet;
2.创建对象
HashSet<Integer> set = new HashSet<>();
- 核心方法(6 个足够)
// 1. 添加元素,重复添加会自动失败 set.add(element);
// 2. 判断是否包含 set.contains(element);
// 3. 删除元素 set.remove(element);
// 4. 大小 set.size();
// 5. 清空 set.clear();
// 6. 是否为空 set.isEmpty();
- 遍历
for (Integer num : set) { // 遍历所有不重复元素 }
HashMap vs HashSet 怎么选
-
要统计次数、存映射关系 → HashMap
-
只要去重、判断是否存在 → HashSet
1. 哈希冲突是什么
- 两个不同 key 算出同一个数组下标
- Java 解决:链地址法(链表)+ 红黑树优化
1. 链地址法(最核心)
- 哈希表底层是一个数组
- 每个数组位置是一个链表头
- 冲突的 key 就挂在这个链表后面
2. JDK 1.8 升级:链表太长 → 转红黑树
为了防止链表太长导致查询退化成 O (n),Java 做了优化:
链表长度 ≥ 8 并且 数组长度 ≥ 64→ 自动转换成 红黑树
树节点数量 ≤ 6→ 退化成链表
为什么这样
链表:短的时候快 红黑树:长的时候查询 O (log n),比 O (n) 快很多
三、除了链地址法,还有哪些解决冲突方式?(面试常问对比)
Java 只用链地址法,但你要知道别的方法:
开放寻址法冲突了就往后找空位置:线性探测、二次探测→ 比如 ThreadLocalMap 用这个
再哈希法冲突就换一个哈希函数再算一次
建立公共溢出区冲突的统一放另一个数组
HashMap 减少冲突的其他手段(高频)
1. 哈希扰动(让 hash 更散列)
Java 不会直接用 key.hashCode(),而是做了高低位异或扰动:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
目的:让哈希值分布更均匀,减少冲突。
2. 负载因子 loadFactor
默认 0.75
意思:数组满 75% 就扩容
- 太大 → 冲突变多
- 太小 → 空间浪费
3. 扩容机制
默认容量 16,扩容 ×2扩容后重新计算所有位置,打散冲突。
| 对比 | 哈希扰动 | 扩容机制 |
|---|---|---|
| 作用对象 | hash 值 | 数组容量 |
| 发生时间 | 每次 put 时 | 满负载时 |
| 结果 | 让 hash 更散 | 数组变大 ×2 |
| 是否迁移数据 | 不迁移 | 必须迁移所有数据 |
| 目的 | 减少冲突 | 整体降低冲突频率 |
| 层级 | 前端优化 | 后端扩容 |
2. 为什么哈希表操作是 O (1)
- 哈希函数直接定位下标,不遍历全表
- 最坏 O (n),但力扣题目全部按平均 O (1) 算
3. 允许 null 吗
- HashMap:key 允许 1 个 null,value 允许多个 null
- HashSet:允许 1 个 null
4. 有序吗
- 无序! 不保证插入顺序
- 要有序用
LinkedHashMap(极少用)
为什么要重写 hashCode () 和 equals ()
面试必考:
- 如果两个对象
equals为 true,hashCode 必须相同 - 否则它们会被当成不同 key,存到不同位置,HashMap 就错乱了
易混点:
| 实现类 | 底层结构 | 是否有序 | 线程安全 | null 允许 | 时间复杂度 |
|---|---|---|---|---|---|
| HashMap | 数组 + 链表 + 红黑树 | 无序 | ❌ | 1 个 null key | O(1) |
| Hashtable | 数组 + 链表 | 无序 | ✅ | 不允许 | O(1) |
| LinkedHashMap | HashMap + 双向链表 | 插入有序 | ❌ | 允许 | O(1) |
| TreeMap | 红黑树 | 按键排序 | ❌ | key 不能为 null | O(log n) |
- 结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!