Menu
CoddyTech

哈希表

最后更新

哈希表把元素存放在一个桶数组中。要放入或查找一个键,先用哈希函数处理这个键,再对桶的数量取模,就能以 O(1) 得到桶的下标。点击上方的“播放”,观察每个键如何被哈希到某个桶中存储,以及查找时如何直接跳到正确的桶。

当两个键哈希到同一个桶时,就发生了冲突。这个可视化使用拉链法:每个桶存一个小列表,冲突的键追加到列表中。只要表不太满(负载因子较低),并且哈希函数能把键分散均匀,链就会保持很短,插入、查找和删除的平均复杂度都是 O(1)。

时间复杂度

操作平均最坏情况
插入O(1)O(n)(所有键都冲突)
查找O(1)O(n)
删除O(1)O(n)
空间O(n)O(n)

核心概念

术语含义
哈希函数把键映射为桶的下标
冲突两个键落在同一个桶中
拉链法每个桶存一个由冲突条目组成的列表
开放寻址法另一种方案:探测下一个空位
负载因子条目数 / 桶数,决定何时扩容

示例演算

用 hash(k) = k % 7 把键 20、34、9、13 插入一个有 7 个桶的表:

步骤结构操作
插入 20桶 6:[20]20 % 7 = 6,桶 6 为空,存入 20
插入 34桶 6:[20, 34]34 % 7 = 6,与 20 冲突,把 34 追加到链上
插入 9桶 2:[9]9 % 7 = 2,桶 2 为空,存入 9
插入 13桶 6:[20, 34, 13]13 % 7 = 6,发生冲突,把 13 追加到桶 6 的链上
查找 34桶 6:[20, 34, 13]34 % 7 = 6,扫描链:20 不是,34 匹配,找到了

什么时候使用哈希表

适合使用的情况不适合使用的情况
需要按键进行平均 O(1) 的插入、查找和删除需要让键保持有序,应使用平衡 BST
键没有有用的顺序,只需判断是否精确存在需要范围查询或查找最接近的键
能为桶和较低的负载因子付出一些额外内存内存极其紧张,每个字节都要计较
你的键类型有好的哈希函数最坏情况下的延迟必须有上限,冲突会让它变成 O(n)

Hash Table 代码

简洁、可运行的 Hash Table 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Hash Table 的 Python 代码

Python
1class HashTable:2    def __init__(self, size=8):3        self.size = size4        self.buckets = [[] for _ in range(size)]5
6    def _index(self, key):7        # Hash the key to a bucket; different keys can collide8        return sum(ord(ch) for ch in key) % self.size9
10    def set(self, key, value):11        bucket = self.buckets[self._index(key)]12        for i, (k, _) in enumerate(bucket):13            if k == key:14                bucket[i] = (key, value)  # update existing key15                return16        bucket.append((key, value))  # chain on collision17
18    def get(self, key):19        for k, v in self.buckets[self._index(key)]:20            if k == key:21                return v22        raise KeyError(key)23
24    def delete(self, key):25        bucket = self.buckets[self._index(key)]26        for i, (k, _) in enumerate(bucket):27            if k == key:28                del bucket[i]29                return30        raise KeyError(key)31
32
33table = HashTable()34table.set("apple", 3)35table.set("banana", 7)36table.set("cherry", 5)37
38print("apple  ->", table.get("apple"))39print("banana ->", table.get("banana"))40table.set("apple", 10)41print("apple  ->", table.get("apple"))42table.delete("banana")43print("bucket sizes:", [len(b) for b in table.buckets])
在 Python 在线编辑器中运行这段代码

哈希表常见问题

什么是哈希冲突?怎么解决?

当两个不同的键哈希到同一个桶时,就发生了冲突。两种常见的解决方法是拉链法(每个桶存一个列表,冲突的键追加进去,也就是这里演示的方法)和开放寻址法(在数组中探测下一个空位)。两者都能保证查找正确,区别在于内存布局和高负载下的性能。

哈希表的时间复杂度是多少?
当哈希函数能把键均匀分布、负载因子保持较低时,插入、查找和删除的平均复杂度都是 O(1)。最坏情况下(所有键都冲突到同一个桶),它们会退化为 O(n),所以好的哈希函数和扩容很重要。
什么是负载因子?

负载因子等于已存条目数除以桶数。它越大,链就越长,操作也越慢,所以大多数哈希表会在它超过某个阈值(如 0.75)时扩容,即重新哈希到一个更大的数组中。

什么时候该用哈希表而不是二叉搜索树?
如果只需要精确匹配的查找,希望平均速度达到 O(1),并且不要求顺序,就用哈希表。如果需要键有序、范围查询,或查找前驱和后继,就用平衡二叉搜索树,这些是哈希表无法高效完成的。BST 能保证 O(log n) 的操作,而哈希表放弃了这种保证,换来更快的平均性能。
拉链法和开放寻址法有什么区别?

拉链法把冲突的键存在每个桶自己的列表中,所以一个桶可以容纳很多条目,表永远不会真正被填满。开放寻址法把所有内容都放在数组本身里,冲突时探测下一个空位,这对缓存很友好,但随着负载因子接近 1,性能会急剧下降,删除也需要小心处理。拉链法能容忍更高的负载因子,开放寻址法的内存使用更紧凑。

为什么不能指望哈希表按插入顺序或排序顺序保存键?
哈希函数会刻意把键分散到各个桶中,避免扎堆,所以迭代顺序反映的是桶的布局,而不是插入顺序或排序顺序。如果需要顺序,请使用有序映射或树,或者按插入顺序保存的映射(例如 Python 的 dict 会保留插入顺序,但这是语言层面的保证,并不是哈希表固有的特性)。除非语言明确承诺,否则永远不要假设迭代顺序和插入键的顺序一致。
Coddy 编程语言插图

在 Coddy 掌握算法

开始