哈希表是一种基于哈希算法实现的高效数据结构,用于存储键值对(key-value pairs)。它的核心思想是通过一个哈希函数(hash function)将键(key)映射到数组的某个位置(索引),然后在该位置存储对应的值(value)。这样,在理想情况下,查找、插入和删除操作的时间复杂度都可以接近 O(1)。
一、哈希表的基本原理
-
哈希函数:
哈希函数的作用是将任意大小的输入(键)转换为一个固定大小的整数值(哈希值)。这个哈希值对应数组的一个索引位置。
例如:hash(key) = index
,其中index
是数组的索引。 -
数组存储:
哈希表内部使用一个数组(通常称为桶数组)来存储数据。每个数组位置称为一个“桶”(bucket)。 -
处理冲突:
由于哈希函数将较大的键空间映射到较小的数组索引空间,不同的键可能映射到相同的索引,这种情况称为“冲突”(collision)。常见的冲突解决方法有两种:- 链地址法(Separate Chaining):
每个数组位置存储一个链表(或其他数据结构,如红黑树)。当发生冲突时,新的键值对会被添加到该索引位置的链表中。 - 开放寻址法(Open Addressing):
当发生冲突时,按照某种探测方法(如线性探测、二次探测、双重哈希)在数组中寻找下一个空闲位置。
- 链地址法(Separate Chaining):
-
基本结构
-
关键组件
-
桶数组(Bucket Array):固定大小的连续存储空间
-
哈希函数(Hash Function):
index = hash(key) % array_size
-
冲突解决机制:处理不同key映射到同一索引的情况
-
-
时间复杂度
操作 | 平均情况 | 最坏情况 |
---|---|---|
查找 | O(1) | O(n) |
插入 | O(1) | O(n) |
删除 | O(1) | O(n) |
二、哈希表操作步骤
插入键值对(Put):
- 计算键的哈希值:
index = hash(key)
- 如果使用链地址法:
- 找到数组对应的索引位置。
- 如果该位置为空,创建一个链表并将键值对放入链表。
- 如果该位置已有链表,则遍历链表:
- 如果链表中已存在相同的键,更新其值。
- 否则,将新的键值对添加到链表末尾。
- 如果使用开放寻址法:
- 从计算出的索引开始,按照探测方法(如线性探测:
index = (index + 1) % array_size
)寻找空闲位置。 - 找到空闲位置后,将键值对放入该位置。
- 从计算出的索引开始,按照探测方法(如线性探测:
获取值(Get):
- 计算键的哈希值:
index = hash(key)
- 链地址法:
- 找到数组索引位置。
- 遍历链表,查找具有相同键的节点,返回对应的值。
- 如果未找到,返回空或错误。
- 开放寻址法:
- 从计算出的索引开始,按照相同的探测方法逐个检查位置,直到找到键相同的项(返回其值)或遇到空位置(说明键不存在)。
删除键值对(Remove):
- 类似查找过程,找到键对应的位置。
- 链地址法:从链表中移除该节点。
- 开放寻址法:通常不能直接删除(否则会破坏探测序列),而是采用标记删除(如用特殊标记代替实际删除)。
理想特性
- 确定性:相同key总是产生相同哈希值
- 均匀性:键值均匀分布在桶中
- 高效性:计算速度快
- 雪崩效应:微小输入变化导致巨大输出变化
常见哈希函数
# 1. 除法哈希
def hash_div(key, size):
return key % size
# 2. 乘法哈希
def hash_mult(key, size, A=0.618):
return int(size * ((key * A) % 1))
# 3. 多项式哈希(字符串)
def hash_poly(s, size):
h = 0
for char in s:
h = (31 * h + ord(char)) % size
return h
三、冲突解决机制
-
链地址法(Separate Chaining)
- 每个桶存储链表/树结构
- Java HashMap使用:链表长度>8时转为红黑树
-
开放寻址法(Open Addressing)
- 线性探测:index = (hash(key) + i) % size
- 二次探测:index = (hash(key) + i*i) % size
- 双重哈希:index = (hash1(key) + i*hash2(key)) % size
-
性能对比
方法 优点 缺点 链地址法 简单,处理冲突灵活 指针开销,内存不连续 开放寻址法 无指针,缓存友好 装载因子限制(<0.7)
四、动态扩容(Rehashing)
当哈希表中的元素数量增加到一定程度(如负载因子超过阈值,负载因子 = 元素数量 / 数组大小),哈希冲突的概率会显著增加,导致性能下降。此时,需要动态扩容:
-
创建一个更大的新数组(通常是原数组大小的两倍)。
-
重新计算每个键值对的哈希值(因为数组大小变了,哈希函数通常与数组大小相关)。
-
将旧数组中的所有键值对重新插入到新数组中。
-
释放旧数组。
扩容过程
def resize(new_size):
new_buckets = [None] * new_size
for entry in old_buckets:
if entry:
new_index = hash(entry.key) % new_size
# 处理冲突并插入新桶
return new_buckets
扩容策略
- 触发条件:装载因子(元素数/桶数) > 阈值(通常0.75)
- 新桶大小:通常取大于当前桶数的最小质数
- 渐进式rehash:Redis使用双哈希表逐步迁移
五、通俗比喻
想象一个图书馆:
- 哈希函数:根据书名生成一个书架编号(例如:将书名首字母映射为数字)。
- 数组:图书馆的书架。
- 冲突处理:
- 链地址法:同一个书架上有一个小本子,记录该书架上的所有书(链表)。如果多个书映射到同一个书架,就在本子上按顺序记录它们的位置。
- 开放寻址法:如果某本书的书架位置已被占用,就按规则(如向右找下一个空书架)放置。
六、经典应用场景
-
数据库索引
CREATE INDEX idx_email ON users(email) USING HASH;
- 等值查询速度远超B+树索引
-
编程语言实现
语言 实现类 冲突解决 Python dict 开放寻址 Java HashMap 链表/红黑树 C++ unordered_map 链地址法 -
缓存系统
-
Redis字典结构:
typedef struct dict { dictht ht[2]; // 双哈希表 long rehashidx; // rehash进度 } dict;
-
-
文件去重
file_hashes = {} for file in files: file_hash = sha256(file_content) if file_hash in file_hashes: # 创建硬链接 else: file_hashes[file_hash] = file_path
七、高级优化技术
-
布谷鸟哈希(Cuckoo Hashing)
-
使用2个哈希函数和2个桶数组
-
插入逻辑:
def insert(key, value): i1 = hash1(key) % size i2 = hash2(key) % size if table1[i1] is empty: store(key, value, table1[i1]) elif table2[i2] is empty: store(key, value, table2[i2]) else: # 驱逐原有键值并重新插入
-
-
一致性哈希(Distributed Systems)
- 节点增减仅影响相邻数据
- 用于Amazon DynamoDB、Cassandra等
-
完美哈希(Static Datasets)
-
两级哈希结构:
struct PerfectHash { uint32_t level1[K]; uint32_t level2[S]; };
-
确保无冲突,适合不变数据集
-
八、实际问题与解决方案
-
哈希洪水攻击
- 问题:恶意构造大量冲突键使性能退化为O(n)
- 解决方案:
- Python:添加随机盐(PYTHONHASHSEED=random)
- Java:链表转红黑树(阈值=8)
-
内存优化
- 开放寻址法:直接存储键值对,无指针开销
- 紧凑存储:Robin Hood Hashing减少探测距离
-
并发控制
- 分段锁:ConcurrentHashMap分16个锁段
- CAS操作:无锁链表头更新
哈希表 vs 其他数据结构
特性 | 哈希表 | 平衡树 | 跳表 |
---|---|---|---|
查找复杂度 | O(1) | O(log n) | O(log n) |
范围查询 | 不支持 | 支持 | 支持 |
内存开销 | 低-中 | 中 | 高 |
有序遍历 | 需额外排序 | 自然有序 | 自然有序 |
实现复杂度 | 中 | 高 | 中 |
哈希表总结
- 极致速度:平均O(1)的访问速度
- 空间高效:装载因子0.7时空间利用率70%+
- 实现灵活:多种冲突解决方案适应不同场景
- 扩展性强:从嵌入式系统到分布式数据库
# Python风格哈希表简化实现
class HashTable:
def __init__(self, capacity=8):
self.capacity = capacity
self.load_factor = 0.75
self.size = 0
self.buckets = [[] for _ in range(capacity)] # 链地址法
def _hash(self, key):
return hash(key) % self.capacity
def put(self, key, value):
if self.size / self.capacity > self.load_factor:
self._resize()
idx = self._hash(key)
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # 更新
return
bucket.append((key, value)) # 新增
self.size += 1
def get(self, key):
idx = self._hash(key)
for k, v in self.buckets[idx]:
if k == key:
return v
return None
def _resize(self):
new_capacity = self.capacity * 2
new_table = HashTable(new_capacity)
for bucket in self.buckets:
for key, value in bucket:
new_table.put(key, value)
self.capacity = new_capacity
self.buckets = new_table.buckets
哈希表通过哈希函数快速定位数据,用数组存储,并用冲突解决方法处理映射重复的问题。它在平均情况下能实现高效的查找、插入和删除操作。