写在前面
哈希表(Hash Table)是计算机科学中最重要的数据结构之一。它能在平均 O(1) 的时间复杂度内完成查找、插入和删除操作——比数组的 O(n) 遍历和二叉搜索树的 O(log n) 都要快得多。
本文将从零开始,用生活类比 + 图解 + 代码的方式,带你彻底理解哈希表的内部工作原理。
一、什么是哈希表?
生活类比
想象你去图书馆找一本《哈利·波特》:
- 笨办法:从第一个书架开始,一本一本地翻,直到找到为止(O(n))
- 聪明办法:图书管理员根据书名的首字母,直接带你到 H 开头的书架区域(O(1))
哈希表就是这个”聪明的图书管理员”——它通过一个哈希函数,把键(Key)直接转换成存储位置,省去了遍历的麻烦。
核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| 键(Key) | 用于查找数据的标识符 | 书名 |
| 值(Value) | 与键关联的实际数据 | 书的内容 |
| 哈希函数 | 将键映射到数组索引的函数 | 图书管理员的索引规则 |
| 哈希值 | 哈希函数的输出(数组下标) | 书架编号 |
二、哈希表的工作原理
整体流程

以电话簿为例,查找 “Tom” 的电话号码:
- 将键
"Tom"输入哈希函数 - 哈希函数计算出索引值
2 - 直接访问数组下标
[2],取出值138xxxx
整个过程不需要遍历,一步到位。
哈希函数的本质
哈希函数的核心任务:将任意大小的输入,映射到固定范围的整数索引。
一个最简单的哈希函数示例(字符串键):
def simple_hash(key: str, table_size: int) -> int: """将字符串中每个字符的 ASCII 码相加,再取余""" total = sum(ord(ch) for ch in key) return total % table_size例如:simple_hash("Tom", 7) = (84 + 111 + 109) % 7 = 304 % 7 = 3
好的哈希函数的特性
| 特性 | 说明 | 重要性 |
|---|---|---|
| 确定性 | 相同输入始终产生相同输出 | 必须 |
| 均匀分布 | 输出尽可能均匀分布在数组范围内 | 必须 |
| 高效计算 | 计算过程简单快速 | 重要 |
| 雪崩效应 | 输入的微小变化导致输出巨大变化 | 加分项 |
三、哈希冲突
为什么会产生冲突?
哈希函数把无限的输入空间映射到有限的数组空间,根据鸽巢原理,冲突是不可避免的。
例如,假设数组大小为 7:
hash("Tom") = 3hash("Max") = 3
两个不同的键映射到了同一个索引——这就是哈希冲突。
解决方法一:链地址法(Separate Chaining)
思路:每个数组位置不再只存一个元素,而是维护一个链表。所有哈希到同一位置的元素,都挂在这个链表上。

class ChainingHashTable: def __init__(self, size=7): self.size = size self.table = [[] for _ in range(size)] # 每个槽位是一个空链表
def _hash(self, key): return hash(key) % self.size
def put(self, key, value): idx = self._hash(key) # 如果 key 已存在,更新 value for i, (k, v) in enumerate(self.table[idx]): if k == key: self.table[idx][i] = (key, value) return # 否则追加到链表末尾 self.table[idx].append((key, value))
def get(self, key): idx = self._hash(key) for k, v in self.table[idx]: if k == key: return v raise KeyError(key)
def delete(self, key): idx = self._hash(key) for i, (k, v) in enumerate(self.table[idx]): if k == key: del self.table[idx][i] return raise KeyError(key)复杂度:
| 操作 | 平均 | 最坏(所有键冲突) |
|---|---|---|
| 查找 | O(1) | O(n) |
| 插入 | O(1) | O(n) |
| 删除 | O(1) | O(n) |
解决方法二:开放定址法(Open Addressing)
思路:冲突发生时,不使用链表,而是按照某种探测序列在数组中寻找下一个空位。
class OpenAddressingHashTable: def __init__(self, size=7): self.size = size self.keys = [None] * size self.values = [None] * size
def _hash(self, key): return hash(key) % self.size
def put(self, key, value): idx = self._hash(key) while self.keys[idx] is not None: if self.keys[idx] == key: self.values[idx] = value # 更新 return idx = (idx + 1) % self.size # 线性探测:找下一个位置 self.keys[idx] = key self.values[idx] = value
def get(self, key): idx = self._hash(key) while self.keys[idx] is not None: if self.keys[idx] == key: return self.values[idx] idx = (idx + 1) % self.size raise KeyError(key)三种探测策略对比
| 策略 | 探测序列 | 优点 | 缺点 |
|---|---|---|---|
| 线性探测 | h, h+1, h+2, … | 实现简单 | 容易聚集(primary clustering) |
| 二次探测 | h, h+1², h+2², … | 减少聚集 | 可能无法探测所有位置 |
| 双重哈希 | h, h+hash₂(key), h+2·hash₂(key), … | 分布最均匀 | 计算开销较大 |
四、哈希表的存储结构
底层数组
哈希表的底层是一个固定大小的数组(称为”桶数组”或”bucket array”)。
索引: [0] [1] [2] [3] [4] [5] [6]值: Amy --- Tom Bob Eve Max ---当数据量接近数组大小时,冲突概率急剧上升,查找退化为 O(n)。此时需要扩容(rehash)。
负载因子
| 负载因子 | 冲突概率 | 性能 |
|---|---|---|
| 0 ~ 0.5 | 低 | 优秀 |
| 0.5 ~ 0.75 | 适中 | 良好 |
| 0.75+ | 高 | 明显退化 |
| 1.0(满) | 100% | 退化为 O(n) |
[!tip] Java HashMap 的阈值 Java 的
HashMap默认负载因子阈值为 0.75。当超过这个值时,数组大小翻倍,所有元素重新哈希(rehash)到新数组中。
扩容(Rehash)过程
def _resize(self): """当负载因子超过阈值时扩容""" old_table = self.table self.size *= 2 # 数组大小翻倍 self.table = [[] for _ in range(self.size)] self.count = 0 for bucket in old_table: for key, value in bucket: self.put(key, value) # 重新哈希所有元素五、常见哈希函数
| 类型 | 公式 | 适用场景 |
|---|---|---|
| 除法取余 | hash = key % table_size | 整数键 |
| 乘法取整 | hash = floor(key * A % 1 * table_size) | 浮点数键 |
| 平方取中 | hash = (key²) // 10^(n/2) % table_size | 数字键 |
| 字符串哈希 | 逐字符加权求和 | 字符串键 |
一个更健壮的字符串哈希函数(DJB2 算法):
def djb2_hash(key: str, table_size: int) -> int: """Dan Bernstein 的经典哈希算法""" hash_val = 5381 for ch in key: hash_val = ((hash_val << 5) + hash_val) + ord(ch) # hash * 33 + c return hash_val % table_size六、完整实现:带扩容的哈希表
class HashMap: """链地址法实现,支持自动扩容"""
def __init__(self, initial_size=7, load_factor=0.75): self.size = initial_size self.load_factor = load_factor self.count = 0 self.table = [[] for _ in range(self.size)]
def _hash(self, key): return hash(key) % self.size
def _check_resize(self): if self.count / self.size > self.load_factor: self._resize()
def _resize(self): old_table = self.table self.size *= 2 self.table = [[] for _ in range(self.size)] self.count = 0 for bucket in old_table: for key, value in bucket: self.put(key, value)
def put(self, key, value): self._check_resize() idx = self._hash(key) bucket = self.table[idx] for i, (k, v) in enumerate(bucket): if k == key: bucket[i] = (key, value) return bucket.append((key, value)) self.count += 1
def get(self, key): idx = self._hash(key) for k, v in self.table[idx]: if k == key: return v raise KeyError(key)
def delete(self, key): idx = self._hash(key) bucket = self.table[idx] for i, (k, v) in enumerate(bucket): if k == key: del bucket[i] self.count -= 1 return raise KeyError(key)
def __repr__(self): items = [] for bucket in self.table: for k, v in bucket: items.append(f"{k}: {v}") return "{" + ", ".join(items) + "}"使用示例:
hm = HashMap()hm.put("Tom", 13800001111)hm.put("Amy", 13900002222)hm.put("Bob", 13600003333)print(hm.get("Tom")) # 13800001111hm.delete("Amy")print(hm) # {Tom: 13800001111, Bob: 13600003333}七、实际应用
| 场景 | 说明 |
|---|---|
| 字典/Map | Python dict、Java HashMap、JS Object |
| 缓存系统 | Redis、Memcached 的核心数据结构 |
| 数据库索引 | 加速 WHERE 条件查询 |
| 去重 | 利用键的唯一性快速判重 |
| 计数器 | 统计词频、访问次数等 |
| 符号表 | 编译器中变量名到内存地址的映射 |
八、总结
| 对比项 | 数组/链表 | 哈希表 |
|---|---|---|
| 查找 | O(n) | O(1) 平均 |
| 插入 | O(n) | O(1) 平均 |
| 删除 | O(n) | O(1) 平均 |
| 有序性 | 有序(数组) | 无序 |
| 空间 | 紧凑 | 需要额外空间 |
哈希表的核心就一句话:用空间换时间。通过哈希函数把查找变成计算,把遍历变成定位,从而实现了常数级的操作速度。
[!note] 参考资料
- 菜鸟教程 - 哈希表
- 《算法(第4版)》— Robert Sedgewick
- Python
dict源码解析
部分信息可能已经过时





