跳到内容

元数据卡

  • 前置知识:哈希表基本原理(上一页)
  • 预计时间:12 分钟
  • 完成标志:能理解链地址法和开放地址法,知道负载因子的作用

冲突不可避免

即使最好的哈希函数,两个不同的 key 也可能映射到同一个数组下标。这叫哈希冲突

解决冲突主要有两种方法。

方法一:链地址法 Separate Chaining

每个数组槽不直接存值,而是存一个链表。冲突的元素追加到链表末尾。

数组:
[0] → (key1, val1) → (key2, val2)  ← key1 和 key2 映射到同一下标
[1] → (key3, val3)
[2] →
python
class HashTableChaining:
    def __init__(self, capacity=16):
        self.capacity = capacity
        self.table = [[] for _ in range(capacity)]
        self.size = 0
    
    def _hash(self, key):
        return hash(key) % self.capacity
    
    def put(self, key, value):
        index = self._hash(key)
        for i, (k, v) in enumerate(self.table[index]):
            if k == key:
                self.table[index][i] = (key, value)  # 更新
                return
        self.table[index].append((key, value))  # 新增
        self.size += 1

优点:实现简单,删除方便 缺点:链表太长时退化成 O(n)

方法二:开放地址法 Open Addressing

不额外用链表,而是冲突时在数组里找下一个空位。

python
class HashTableOpenAddr:
    def __init__(self, capacity=16):
        self.capacity = capacity
        self.table = [None] * capacity
        self.size = 0
    
    def _hash(self, key):
        return hash(key) % self.capacity
    
    def put(self, key, value):
        index = self._hash(key)
        while self.table[index] is not None:
            if self.table[index][0] == key:
                self.table[index] = (key, value)  # 更新
                return
            index = (index + 1) % self.capacity  # 线性探测
        self.table[index] = (key, value)
        self.size += 1

线性探测就是冲突后依次往后找空位。如果第 3 个槽被占了,就看第 4 个,第 5 个……直到找到空位。

优点:空间利用率高 缺点:容易产生"聚集"——连续被占的槽越来越多

负载因子与扩容

哈希表不能无限制地往里塞数据。负载因子(load factor) 是已存元素 / 总容量。

python
load_factor = size / capacity
  • 负载因子 0.5:容量用了一半
  • 负载因子 0.75:Java HashMap 的默认阈值
  • 负载因子接近 1:冲突概率大增,性能崩溃

当负载因子超过阈值时,哈希表需要扩容(rehash)

  1. 创建一个更大的数组(通常是 2 倍)
  2. 把旧数组的所有元素重新计算哈希,放入新数组
python
def _resize(self):
    old_table = self.table
    self.capacity *= 2
    self.table = [[] for _ in range(self.capacity)]
    self.size = 0
    for bucket in old_table:
        for key, value in bucket:
            self.put(key, value)

扩容是 O(n) 的操作,但平均分摊到每次插入,仍然是 O(1)。这叫摊还分析

真实世界的哈希表

语言实现冲突解决负载因子备注
Java HashMap数组 + 链表/红黑树链地址0.75链表>8 转红黑树
Python dict数组 + 开放地址开放地址2/3伪随机探测序列
C++ unordered_map数组 + 链表链地址1.0逐个桶链式存储

旅人笔记

冲突不可避免。链地址法用链表兜底(简单),开放地址法在数组里找空位(省空间)。负载因子控制扩容时机——太小浪费空间,太大影响性能。扩容是 O(n) 但平均下来仍然是 O(1)。

下一步:二叉树

哈希表适合精确查找——给我一个 key,立刻拿到值。但如果你需要按顺序遍历数据呢?

看二叉树 →

Built with VitePress | Software Systems Atlas