程序员面试宝典

一站式面试准备平台

返回分类
algorithms中级

哈希表详解

深入理解哈希函数的设计、哈希冲突的处理、以及哈希表在面试中的应用

2026-04-15
阅读时间: 11分钟

哈希表详解

哈希表是面试中最常用的数据结构之一。关键不是背实现,而是理解为什么哈希表能 O(1) 查找,以及哈希冲突是什么、怎么处理

面试考察点

面试官通过这道题想考察:
1. 你是否理解哈希表的核心思想
2. 你是否能解释什么是哈希冲突
3. 你是否理解不同冲突解决方法的特点
4. 你是否能根据场景设计合适的哈希函数

一、哈希表的核心思想

1.1 什么是哈希表?(小白解释)

面试官追问:"为什么哈希表查找是 O(1)?"

哈希表 = 数组 + 哈希函数

核心思想:
通过哈希函数,把"键"转换成数组"索引"
然后直接用索引访问数组

类比:
- 普通数组:知道学号,知道在第几个,但还是要 O(n) 找
- 哈希表:知道学号,直接算出在哪个"桶"里

举例:存成绩

学号:1 → 哈希函数 → 位置 1 → 存 95 分
学号:2 → 哈希函数 → 位置 2 → 存 87 分
学号:3 → 哈希函数 → 位置 3 → 存 92 分

查找时:
学号 2 → 哈希函数 → 位置 2 → 立即得到 87 分
          ↑
      一次计算就能定位!

1.2 什么是哈希函数?

哈希函数:把任意输入转换成固定范围的整数

好哈希函数的特点:
1. 确定性:相同输入 → 相同输出
2. 均匀性:输出均匀分布
3. 高效性:计算速度快

简单哈希函数示例:
int hash(key, size) {
    return key % size;  // 取模
}

问题:如果 key = 100, size = 10,hash = 0
      如果 key = 200, size = 10,hash = 0
      冲突!

面试加分点:
好的哈希函数应该让结果分布均匀,
Java String 的 hashCode 用的是:
h = s[0]*31^(n-1) + s[1]*31^(n-2) + ...
这能让字符的排列得到不同的哈希值

二、哈希冲突(面试核心)

2.1 什么是哈希冲突?

面试官追问:"什么是哈希冲突?为什么会发生?"

哈希冲突:不同的 key 通过哈希函数得到相同的哈希值

鸽巢原理:
如果有 10 个桶,11 只鸽子,
至少有 1 个桶有 2 只或更多鸽子。

同理:
如果哈希值范围是 0-9,
但要存储 11 个元素,
至少有 2 个元素哈希值相同!

冲突是不可避免的,只能处理!

2.2 冲突解决方法 1:链地址法(Separate Chaining)

面试官追问:"链地址法是怎么处理冲突的?"

链地址法:冲突的元素放在同一个桶的链表里

结构:
桶 0: [key1, key2, ...] → null
桶 1: [key3] → null
桶 2: [] → null
...

插入:算出哈希值,加到对应桶的链表头部
查找:算出哈希值,在对应链表里遍历

时间复杂度:
- 理想情况(哈希均匀):O(1) 找到桶,O(k/n) 遍历链表
  k = 元素总数,n = 桶数
  如果桶数足够多,链表很短 → 接近 O(1)
- 最坏情况(全部冲突):O(k)

优点:实现简单,删除容易
缺点:链表长时退化
图解:

    哈希函数: key % 5

    桶0: [0, 5, 10] → null
    桶1: [1, 6] → null
    桶2: [2, 7] → null
    桶3: [3] → null
    桶4: [4, 9] → null

    插入 12: 12 % 5 = 2 → 加到桶2

2.3 冲突解决方法 2:开放地址法(Open Addressing)

面试官追问:"开放地址法怎么处理冲突?"

开放地址法:冲突时,找另一个空位置

探测序列:h(0), h(1), h(2)...

1. 线性探测:依次往后找空位
   h(key) + 0, h(key) + 1, h(key) + 2, ...
   优点:简单
   缺点:容易产生"聚集"(连续位置被占)

2. 平方探测:探测 h(key) + 1², h(key) - 1², h(key) + 2², ...
   优点:避免聚集
   缺点:可能探测不到所有位置

3. 双重哈希:h(key) + i * h2(key)
   用两个哈希函数
   优点:分布更均匀
图解 - 线性探测:

哈希函数: h(key) = key % 5

依次插入 12, 8, 5, 9:

12 % 5 = 2,位置 2 是空的,放
12: [_, _, 12, _, _]

8 % 5 = 3,位置 3 是空的,放
8:  [_, _, 12, 8, _]

5 % 5 = 0,位置 0 是空的,放
5:  [5, _, 12, 8, _]

9 % 5 = 4,位置 4 是空的,放
9:  [5, _, 12, 8, 9]

2.4 两种方法的对比

┌─────────────────────────────────────────────────┐
│                冲突解决方法对比                    │
├─────────────────────────────────────────────────┤
│                                                  │
│   链地址法                开放地址法              │
│   ─────────                ──────────            │
│   用链表存冲突元素          用空槽存冲突元素       │
│   桶数量固定               负载因子超过阈值需扩容  │
│   删除容易                  删除复杂(标记删除)   │
│   适合不确定数据量          适合确定数据量         │
│                                                  │
└─────────────────────────────────────────────────┘

三、哈希表的扩容

3.1 为什么要扩容?

面试官追问:"哈希表什么时候扩容?为什么?"

负载因子 = 元素数量 / 桶数量

负载因子越大,冲突越多!
- 负载因子 0.5:一半一半,冲突较多
- 负载因子 0.8:非常拥挤
- 负载因子 1.0:满了!

当负载因子超过阈值(通常是 0.75),
就需要扩容!

扩容后:
- 桶数量增加
- 负载因子降低
- 冲突减少

但扩容有代价:需要重新哈希所有元素!
这步是 O(n) 的。

所以:平均 O(1) 插入,偶尔 O(n) 扩容

3.2 扩容时为什么需要重新哈希?

因为哈希值是对桶数量取模!

扩容前:桶数量 = 5
key = 7, 7 % 5 = 2

扩容后:桶数量 = 10
key = 7, 7 % 10 = 7  ← 位置变了!

所以必须重新计算所有元素的哈希值!

四、面试应用

4.1 两数之和(LeetCode 1)

考察点:哈希表的实际应用

问题:数组中找和为 target 的两个数

方法对比:

方法1 - 暴力:
两层循环,O(n²)

方法2 - 排序 + 双指针:
先排序,O(n log n)

方法3 - 哈希表:
遍历数组,对每个数,检查 target - num 是否在哈希表里
是 → 找到
否 → 把当前数加入哈希表

时间:O(n)
空间:O(n)

面试加分点:能解释为什么哈希表能解决这个问题
"因为对于每个数,我只需要知道"配对的那个数是否存在"
这是 O(1) 的查询操作!"

4.2 LRU 缓存(LeetCode 146)

考察点:哈希表 + 双向链表

问题:实现 LRU 缓存

需要操作:
1. get(key) - O(1)
2. put(key, value) - O(1)

O(1) 的 get → 哈希表
O(1) 的 删除"最近最少使用" → 双向链表

为什么用双向链表?
- 删除操作需要找到前驱节点
- 单链表无法 O(1) 找到前驱
- 双向链表可以!

结构:
哈希表 → 双向链表(按访问顺序排列)
        最近使用的在头部
        最久未使用的在尾部

五、面试总结

哈希表核心要点

┌─────────────────────────────────────────────────┐
│                    哈希表                        │
├─────────────────────────────────────────────────┤
│                                                  │
│  核心思想:哈希函数 + 数组 = O(1) 查找          │
│                                                  │
│  冲突处理:                                      │
│  - 链地址法:用链表存冲突元素                    │
│  - 开放地址法:找空位置存                        │
│                                                  │
│  扩容:当负载因子 > 0.75,扩容 + 重新哈希        │
│                                                  │
│  应用:                                          │
│  - 两数之和                                     │
│  - LRU 缓存                                     │
│  - 字符串计数                                   │
│                                                  │
└─────────────────────────────────────────────────┘

面试能加分的回答

1. 能解释为什么是 O(1)
   "通过哈希函数直接计算位置,不需要遍历"

2. 能解释冲突处理
   "链地址法用链表存冲突元素,开放地址法找空位"

3. 能解释扩容
   "负载因子超过阈值时扩容,通常是 0.75"

4. 能设计哈希表 + 链表的组合
   "LRU 缓存用哈希表 O(1) 查找,用双向链表 O(1) 删除"

相关题目

相关标签