哈希表相关面经总结
哈希表面经 没空整理了 先把ai复制粘贴
·
问题1:集合为什么不可以作为哈希的key!
集合(如 Python 的 set、Java 的 Set)不适合做哈希表的 key,根本原因是:集合本身是可变的(mutable),而哈希表对 key 的要求是**“不可变 + 可哈希”**。下面分点说明:
✅ 哈希表对 key 的要求
- 不可变(immutable):
key 在生命周期内不能发生变化,否则哈希值会变,导致查不到原来的条目。 - 可哈希(hashable):
必须实现__hash__()和__eq__()方法,且相等对象哈希值必须相等。
❌ 集合为什么不行
| 特性 | 集合(set) | 要求 |
|---|---|---|
| 可变性 | ✅ 可变(可以增删元素) | ❌ 必须不可变 |
| 可哈希性 | ❌ 未实现 __hash__(),直接调用会抛 TypeError: unhashable type: 'set' |
❌ 必须可哈希 |
🔍 举个例子(Python)
>>> s = {1, 2, 3}
>>> d = {s: "value"}
TypeError: unhashable type: 'set'
解释:集合 s 是可变的,Python 禁止把它当 key。
✅ 替代方案
如果确实需要“像集合一样”的 key,可以用不可变集合:
- Python:
frozenset({1, 2, 3}) - Java:
Collections.unmodifiableSet()+ 封装成不可变对象,或自定义不可变类。
一句话总结
集合本身可变,哈希值会随内容变化,破坏哈希表的不变约定,因此不能作为 key。
问题2:如何降低哈希冲突的风险?
哈希冲突(Hash Collision)无法绝对避免,只能降低概率、缓解影响。
“拒绝”在现实中应理解为:把冲突概率压到足够低,并保证即使冲突也能快速处理。
下面按“事前降低概率 → 事后处理冲突 → 线上监控”三段给出一套工程上常用的组合拳。
一、事前:把冲突概率压下去
- 选高质量哈希函数
- 整数:Murmur3、XXHash、CityHash、FarmHash…
- 字符串:Java 9+ 的
hashCode已换用 Murmur3;Redis 6 默认 SipHash; - 加密场景用 SipHash 抗 DoS(哈希洪水攻击)。
- 让 key 的熵足够大
- 业务 key 过短(如连续订单号)时,加盐(salt)或前缀,再算哈希。
- 负载因子 loadFactor 别太高
- Java
HashMap默认 0.75;Gomap6.5; - 写多读少可降到 0.5,冲突概率指数级下降。
- Java
- 提前估算容量,减少 rehash
new HashMap<>(expectedSize / 0.75 + 1);- 避免运行时频繁扩容触发再哈希。
二、事后:冲突真的来了怎么办
- 链地址法(Separate Chaining)——最通用
- JDK
HashMap采用 链表 + 红黑树(冲突 >8 转树,<6 退链表)。 - 时间复杂度:平均 O(1),最差 O(log n)。
- JDK
- 开放寻址法(Open Addressing)——CPU 缓存友好
- 线性探测、平方探测、双重哈希;
- Go、Rust、C++
unordered_map部分实现采用; - 负载因子必须更低(≤0.5),删除逻辑复杂。
- 再哈希(Rehash)/双重哈希
- 准备 2 个不同算法,第一次冲突用第二个算索引;
- 工程上较少单独使用,多作为开放寻址的子策略。
- 扩容(Resize)——终极手段
- 当冲突链长度超过阈值(如 JDK 的 TREEIFY_THRESHOLD=8)且表长 < 64 时,优先扩容到 2× 而不是转树;
- 扩容后所有 key 重新散列,冲突自然下降。
三、线上:监控 + 防御
- 监控最长冲突链
- JDK 可通过 JFR 或
HashMap$TreeNode计数; - Redis 用
DEBUG HTSTATS <dbid>看最大桶深。
- JDK 可通过 JFR 或
- 防哈希洪水攻击
- 使用 随机化哈希(hashDoS)——Python 3.3+ 默认对 str 加盐;
- 对不可信输入强制 SipHash。
- 压测
- 用 HashDoS 样本(大量冲突字符串)打流量,观察 99th 延迟是否暴涨。
一句话总结
“拒绝”哈希冲突 = 选好哈希函数 + 控制负载因子 + 冲突后转树/扩容 + 线上监控,
而不是追求零冲突——那是数学上不可能的任务。
更多推荐


所有评论(0)