哈希表

键值映射与哈希冲突处理

一、哈希表概述

哈希表(Hash Table),又称散列表,是一种基于键(Key)直接访问存储位置的数据结构。它通过哈希函数(Hash Function)将键映射为数组下标,从而实现近乎 O(1) 平均时间复杂度的查找、插入和删除操作。

哈希表的核心思想非常简单:我们希望设计一种数据结构,能够根据给定的键以极快的速度找到对应的值。在数组和链表中,查找需要遍历所有元素(O(n)),而在平衡二叉树中,查找需要 O(log n) 的比较操作。哈希表则更进一步,通过巧妙的映射将时间复杂度降至常数级别。

核心概念

  • 桶(Bucket): 哈希表中的每个存储位置称为一个桶,通常对应数组中的一个槽位
  • 哈希函数: 将任意大小的键映射为固定范围整数的函数,决定了键值对的存储位置
  • 哈希冲突: 两个不同的键经过哈希函数计算后得到相同的索引值
  • 负载因子(Load Factor): α = n / m,其中 n 为元素数量,m 为桶数量,是衡量哈希表填满程度的关键指标

哈希表在计算机科学中应用极其广泛,几乎所有高级编程语言的标准库都提供了哈希表实现——Python 的 dict、Java 的 HashMap、C++ 的 unordered_map、JavaScript 的 MapObject,其底层核心都是哈希表。

哈希表是时间与空间的经典权衡——它用额外的内存空间换取了近乎常数时间的查找效率。理解哈希表的设计哲学,是理解计算机系统性能优化的关键一步。

二、哈希函数设计

哈希函数的质量直接决定了哈希表的性能。一个好的哈希函数应当满足以下要求:

2.1 除留余数法(Division Method)

最常用的哈希函数形式,其定义为:

h(k) = k mod m

其中 m 是哈希表的大小。选择 m 是关键——m 应为质数,且不要接近 2^p,否则哈希值的低位会具有周期性模式,增加碰撞概率。

# 除留余数法实现 def division_hash(key, table_size): """除留余数法:h(k) = k mod m 选择质数作为表大小可以降低冲突概率""" return hash(key) % table_size # 演示不同m值对分布的影响 def test_distribution(keys, table_size): distribution = {i: 0 for i in range(table_size)} for k in keys: h = division_hash(k, table_size) distribution[h] += 1 return distribution keys = list(range(1000)) # 使用质数17 vs 非质数16 观察分布均匀程度 dist_prime = test_distribution(keys, 17) # 均匀分布 dist_power = test_distribution(keys, 16) # 分布可能不均匀

Python 内置的 hash() 函数在设计上已经考虑了均匀性问题,它会对整数返回自身(但 -1 被映射为 -2 以兼容某些内部实现),对字符串和字节序列使用改良版的 Fowler-Noll-Vo 或 SIP 哈希算法。

2.2 乘法哈希(Multiplication Method)

乘法哈希由 Donald Knuth 在其经典著作《计算机程序设计艺术》中提出,其定义为:

h(k) = ⌊m (kA mod 1)⌋

其中 A 是一个介于 0 和 1 之间的常数。Knuth 建议使用黄金分割比例的倒数:

A = (√5 − 1) / 2 ≈ 0.6180339887

import math def multiplication_hash(key, table_size): """乘法哈希 - 使用黄金分割比例""" A = (math.sqrt(5) - 1) / 2 # 约 0.618 # 提取 kA 的小数部分,再乘以表大小 fractional_part = (hash(key) * A) % 1 return int(table_size * fractional_part) # 乘法哈希的一个优点是对 m 的取值不敏感 # 即使 m 是 2 的幂次,也能获得较好的分布 for key in ["apple", "banana", "cherry", "date"]: h = multiplication_hash(key, 16) print(f"hash('{key}') = {h}")

2.3 一致性哈希(Consistent Hashing)

一致性哈希是一种特殊的哈希函数,专为分布式缓存系统设计。传统哈希在增加或删除节点时,大部分键需要重新映射,导致大规模数据迁移。一致性哈希通过哈希环(Hash Ring)的概念,将重新映射的键数量控制在 O(K/N) 级别(K 为键总数,N 为节点数)。

import hashlib import bisect class ConsistentHashRing: """一致性哈希环实现 使用虚拟节点解决数据倾斜问题""" def __init__(self, nodes=None, virtual_nodes=150): self.virtual_nodes = virtual_nodes self.ring = {} # 哈希值 → 物理节点 self.sorted_keys = [] # 已排序的哈希值列表 if nodes: for node in nodes: self.add_node(node) def _hash(self, key): return int(hashlib.md5(key.encode()).hexdigest(), 16) def add_node(self, node): """为物理节点创建多个虚拟节点,均匀分布在环上""" for i in range(self.virtual_nodes): virtual_key = f"{node}:{i}" h = self._hash(virtual_key) self.ring[h] = node self.sorted_keys.append(h) self.sorted_keys.sort() def remove_node(self, node): """移除物理节点及其所有虚拟节点""" for i in range(self.virtual_nodes): virtual_key = f"{node}:{i}" h = self._hash(virtual_key) del self.ring[h] self.sorted_keys.remove(h) def get_node(self, key): """查找键对应的节点:沿环顺时针找到第一个不小于hash(key)的节点""" if not self.ring: return None h = self._hash(key) idx = bisect.bisect_left(self.sorted_keys, h) if idx == len(self.sorted_keys): idx = 0 # 环状结构:回绕到第一个节点 return self.ring[self.sorted_keys[idx]] # 使用示例 nodes = ["server-1", "server-2", "server-3"] ring = ConsistentHashRing(nodes) print(ring.get_node("user-1001")) # 分配到某个节点 print(ring.get_node("file-photo-2024"))

一致性哈希的实际应用

  • Redis Cluster: 使用哈希槽(Hash Slot)的概念将键分布到 16384 个槽中
  • Amazon DynamoDB: 使用一致性哈希进行数据分区和副本管理
  • Akamai CDN: 将用户请求映射到最近的缓存服务器
  • Cassandra / Riak: 分布式 NoSQL 数据库的核心分片策略

三、哈希冲突解决

无论哈希函数设计得多好,由于桶的数量有限而键空间是无限的,哈希冲突不可避免。处理冲突的策略是哈希表设计的核心环节。常见的方法有三种:链地址法、开放地址法和再哈希法。

3.1 链地址法(Separate Chaining)

链地址法将每个桶实现为一个链表(或其他动态数据结构)。当多个键映射到同一桶时,它们被依次存储在链表中。这种方法实现简单,且对负载因子不敏感,适用于元素数量变化较大的场景。

class HashTableChaining: """链地址法实现的哈希表 每个桶维护一个链表(Python list)存储冲突元素""" 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 insert(self, key, value): 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.size += 1 def get(self, key): idx = self._hash(key) bucket = self.table[idx] for k, v in bucket: 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: bucket.pop(i) self.size -= 1 return raise KeyError(key) def __contains__(self, key): try: self.get(key) return True except KeyError: return False def __len__(self): return self.size # 测试链地址法哈希表 ht = HashTableChaining(capacity=8) ht.insert("apple", 1) ht.insert("banana", 2) ht.insert("cherry", 3) print(ht.get("banana")) # 输出: 2 print("apple" in ht) # 输出: True ht.delete("apple") print("apple" in ht) # 输出: False

3.2 开放地址法(Open Addressing)

开放地址法将所有元素直接存储在哈希表的数组中,不依赖外部链表。当发生冲突时,通过探测序列寻找下一个空桶。根据探测方式的不同,分为线性探测、平方探测和双重哈希三种主要变体。

class HashTableOpenAddressing: """开放地址法(线性探测)实现的哈希表 使用特殊标记处理删除后的空位问题""" _DELETED = object() # 删除标记哨兵 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 _linear_probe(self, key, i): """线性探测:h(k,i) = (h(k) + i) mod m""" return (self._hash(key) + i) % self.capacity def _quadratic_probe(self, key, i): """平方探测:h(k,i) = (h(k) + c1·i + c2·i²) mod m""" c1, c2 = 1, 3 return (self._hash(key) + c1 * i + c2 * i * i) % self.capacity def insert(self, key, value, probe="linear"): probe_fn = self._linear_probe if probe == "linear" else self._quadratic_probe for i in range(self.capacity): idx = probe_fn(key, i) entry = self.table[idx] if entry is None or entry is self._DELETED: self.table[idx] = (key, value) self.size += 1 return if entry is not self._DELETED and entry[0] == key: self.table[idx] = (key, value) # 更新 return raise Exception("哈希表已满,无法插入") def get(self, key, probe="linear"): probe_fn = self._linear_probe if probe == "linear" else self._quadratic_probe for i in range(self.capacity): idx = probe_fn(key, i) entry = self.table[idx] if entry is None: raise KeyError(key) if entry is not self._DELETED and entry[0] == key: return entry[1] raise KeyError(key) def delete(self, key, probe="linear"): probe_fn = self._linear_probe if probe == "linear" else self._quadratic_probe for i in range(self.capacity): idx = probe_fn(key, i) entry = self.table[idx] if entry is None: raise KeyError(key) if entry is not self._DELETED and entry[0] == key: self.table[idx] = self._DELETED self.size -= 1 return raise KeyError(key) def load_factor(self): return self.size / self.capacity

开放地址法三种探测方式对比

  • 线性探测(Linear Probing): 最简单,但容易产生一次聚集(Primary Clustering)现象——连续占用的桶形成长序列,导致后续插入和查找需要探测更多步
  • 平方探测(Quadratic Probing): 以平方步长跳跃,缓解一次聚集,但可能产生二次聚集(Secondary Clustering)——相同哈希值的键仍遵循相同探测路径
  • 双重哈希(Double Hashing): 使用第二个哈希函数决定探测步长,几乎完全消除聚集现象,是理论上最优的开放地址法方案

3.3 再哈希法(Rehashing)

当哈希表的负载因子超过某一阈值时,需要扩容(Resize)重新哈希(Rehash)所有已有元素。这个过程涉及创建更大的新表,将旧表中的每个元素重新计算哈希值并插入新表。虽然扩容操作本身是 O(n) 的,但均摊到每次插入操作后,仍能保持 O(1) 的均摊时间复杂度。

class HashTableWithResize: """支持自动扩容的哈希表 当负载因子超过阈值时自动触发扩容和再哈希""" LOAD_FACTOR_THRESHOLD = 0.75 GROWTH_FACTOR = 2 def __init__(self, capacity=8): self.capacity = capacity self.table = [[] for _ in range(capacity)] self.size = 0 @property def load_factor(self): return self.size / self.capacity def _resize(self): """扩容到当前容量的2倍,并重新哈希所有键值对""" old_table = self.table self.capacity *= self.GROWTH_FACTOR self.table = [[] for _ in range(self.capacity)] self.size = 0 for bucket in old_table: for key, value in bucket: self.insert(key, value) print(f"[扩容] 新容量: {self.capacity}") def insert(self, key, value): if self.load_factor >= self.LOAD_FACTOR_THRESHOLD: self._resize() idx = hash(key) % self.capacity bucket = self.table[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 = hash(key) % self.capacity for k, v in self.table[idx]: if k == key: return v raise KeyError(key) # 演示自动扩容过程 ht = HashTableWithResize(capacity=4) for i in range(10): ht.insert(f"key-{i}", i) print(f"插入 key-{i}: size={ht.size}, " f"load_factor={ht.load_factor:.3f}")

四、Python字典底层实现

Python 的字典(dict)是语言中最核心也最精妙的数据结构之一。CPython 的实现经历了多次重大优化,其底层机制是理解 Python 性能特征的关键。

4.1 PyDictObject 结构

在 CPython 源码中,字典由 PyDictObject 结构体表示。从 Python 3.6 开始,字典采用了全新的紧凑表示法(Compact Representation),由两个核心数组组成:

# Python 3.7+ 字典的特性演示 d = {} # 特性1:保留插入顺序(Python 3.7+ 语言特性) d["name"] = "Alice" d["age"] = 25 d["city"] = "Beijing" print(list(d.keys())) # ['name', 'age', 'city'] # 特性2:动态扩容 import sys d2 = {} prev_size = sys.getsizeof(d2) for i in range(1, 1001): d2[i] = str(i) new_size = sys.getsizeof(d2) if new_size != prev_size: print(f"元素数={i}: 字典大小从 {prev_size} 变为 {new_size} 字节") prev_size = new_size # 特性3:键必须是可哈希的 hash("hello") # 字符串哈希 hash(42) # 整数哈希 hash((1, 2, 3)) # 元组哈希(可哈希) # hash([1, 2, 3]) # TypeError: unhashable type: 'list' # 特性4:自定义哈希 class Point: def __init__(self, x, y): self.x = x self.y = y def __hash__(self): # 使用元组的哈希值组合两个维度 return hash((self.x, self.y)) def __eq__(self, other): return (self.x == other.x) and (self.y == other.y) def __repr__(self): return f"Point({self.x}, {self.y})" p1 = Point(1, 2) p2 = Point(1, 2) d3 = {p1: "origin"} print(d3[p2]) # 输出: "origin" (p1 == p2 且哈希值相同)

4.2 哈希表结构与扩容机制

CPython 字典的哈希表在 Python 3.6+ 中采用了"分拆式"设计:

# 模拟Python字典底层的紧凑哈希表结构 class CompactDict: """模拟 Python 3.7+ 字典的紧凑哈希表实现""" def __init__(self): self.capacity = 8 # 2的幂:indices数组大小 self.mask = self.capacity - 1 self.indices = [-1] * self.capacity # 稀疏索引数组 self.entries = [] # 密集条目数组:[(hash, key, value), ...] self.size = 0 def __setitem__(self, key, value): h = hash(key) idx = h & self.mask # 检查键是否已存在 orig_idx = self.indices[idx] if orig_idx != -1: entry_idx = orig_idx if self.entries[entry_idx][1] == key: self.entries[entry_idx] = (h, key, value) return # 线性探测处理冲突(简化版) while self.indices[idx] != -1: entry_idx = self.indices[idx] if self.entries[entry_idx][1] == key: self.entries[entry_idx] = (h, key, value) return idx = (idx + 1) & self.mask # 插入新条目 self.indices[idx] = len(self.entries) self.entries.append((h, key, value)) self.size += 1 def __getitem__(self, key): h = hash(key) idx = h & self.mask for _ in range(self.capacity): entry_idx = self.indices[idx] if entry_idx == -1: raise KeyError(key) entry_hash, entry_key, entry_val = self.entries[entry_idx] if entry_key == key: return entry_val idx = (idx + 1) & self.mask raise KeyError(key) def __repr__(self): items = ", ".join(f"{k!r}: {v!r}" for _, k, v in self.entries) return f"CompactDict({{{items}}})" # 测试紧凑字典 cd = CompactDict() cd["name"] = "Bob" cd["age"] = 30 cd["score"] = 95 print(cd) # CompactDict({'name': 'Bob', 'age': 30, 'score': 95}) print(cd["age"]) # 30

Python字典的关键优化(Python 3.6+)

  • 内存节省: 紧凑表示法比旧版(Python 3.5 及之前)节省约 30%-50% 的内存
  • 顺序保留: 由于 entries 数组按插入顺序追加,字典天然保留插入顺序(Python 3.7 起为语言规范)
  • 扩容策略: 当负载因子超过 2/3(约 66.7%)时触发扩容,新容量为旧容量的 2 倍,重新构建 indices 数组但保留 entries 顺序
  • 键共享: 同一类的不同实例如果键集合相同,可以共享 keys 部分,进一步节省内存

五、时间复杂度分析

哈希表的时间复杂度是理解其性能特征的关键。下表中总结了哈希表各种操作在不同情况下的时间复杂度:

操作 平均情况 最坏情况 说明
查找(Search) O(1) O(n) 最坏情况所有键映射到同一桶
插入(Insert) O(1) O(n) 均摊后仍为 O(1)(扩容均摊)
删除(Delete) O(1) O(n) 查找 + 移除,查找为瓶颈
扩容(Resize) O(n) 需重新哈希所有元素
遍历(Iteration) O(n) O(n) 需访问所有桶或所有元素

均摊分析(Amortized Analysis)

虽然扩容操作本身是 O(n) 的,但通过"倍增扩容"策略——每次容量翻倍——可以将扩容代价均摊到每次插入操作中。假设初始容量为 m,插入 n 个元素,扩容总代价为 O(m + 2m + 4m + ... + n) = O(n),均摊到每次插入为 O(1)

负载因子 α = n/m 决定了哈希表的性能退化程度:

六、实际应用

哈希表是计算机科学中应用最广泛的数据结构之一,几乎所有需要快速查找的场景都离不开它。

6.1 LRU 缓存(Least Recently Used Cache)

LRU 缓存是哈希表与双向链表结合的经典设计——哈希表提供 O(1) 的键查找,双向链表维护访问顺序。Python 的 collections.OrderedDict 底层正是基于这一设计。

from collections import OrderedDict class LRUCache: """使用 OrderedDict 实现 LRU 缓存 get 和 put 操作均为 O(1)""" def __init__(self, capacity): self.cache = OrderedDict() self.capacity = capacity def get(self, key): if key not in self.cache: return -1 # 移动到末尾表示最近使用 self.cache.move_to_end(key) return self.cache[key] def put(self, key, value): if key in self.cache: self.cache.move_to_end(key) self.cache[key] = value if len(self.cache) > self.capacity: # 弹出最久未使用的(第一个元素) self.cache.popitem(last=False) # 测试 LRU 缓存 cache = LRUCache(capacity=3) cache.put("A", 1) # 缓存: [A] cache.put("B", 2) # 缓存: [A, B] cache.put("C", 3) # 缓存: [A, B, C] cache.get("A") # 访问 A, 缓存: [B, C, A] cache.put("D", 4) # B 被淘汰, 缓存: [C, A, D] print(cache.get("B")) # -1 (已被淘汰)

6.2 去重(Deduplication)

哈希表可以快速判断元素是否存在,因此是实现去重逻辑的理想工具。利用哈希集合 O(1) 的成员检测能力,我们可以在线性时间内完成大规模数据的去重。

def remove_duplicates(items): """使用哈希表实现 O(n) 时间复杂度的去重""" seen = set() result = [] for item in items: if item not in seen: seen.add(item) result.append(item) return result # 使用示例:URL 去重 urls = ["http://a.com", "http://b.com", "http://a.com", "http://c.com", "http://a.com", "http://b.com"] unique_urls = remove_duplicates(urls) print(unique_urls) # 输出: ['http://a.com', 'http://b.com', 'http://c.com'] # 进阶:在文件处理中去重(逐行读取,避免内存爆炸) def dedup_large_file(input_path, output_path): seen = set() with open(input_path, "r", encoding="utf-8") as fin, \ open(output_path, "w", encoding="utf-8") as fout: for line in fin: line = line.strip() if line and line not in seen: seen.add(line) fout.write(line + "\n")

6.3 数据库索引

哈希索引是数据库中最重要的索引类型之一(仅次于 B-Tree)。它在等值查询(如 WHERE id = 42)中表现极佳,适合精确匹配和唯一性约束场景。

# 模拟数据库哈希索引 class HashIndex: """模拟数据库的哈希索引,支持等值查询""" def __init__(self, column_name): self.column_name = column_name self.index = {} # 值 → [row_id_list] def insert(self, row_id, value): if value not in self.index: self.index[value] = [] self.index[value].append(row_id) def equality_lookup(self, value): """等值查询:O(1) 平均""" return self.index.get(value, []) def delete(self, row_id, value): if value in self.index: self.index[value].remove(row_id) if not self.index[value]: del self.index[value] # 模拟数据库记录 records = [ (1, "Alice", 25), (2, "Bob", 30), (3, "Charlie", 25), (4, "David", 35), ] # 在 age 列上建立哈希索引 age_index = HashIndex("age") for row_id, name, age in records: age_index.insert(row_id, age) # 查询年龄为 25 的所有记录 print(age_index.equality_lookup(25)) # [1, 3]

除了上述场景,哈希表还被广泛应用于:

七、负载因子与性能调优

负载因子(Load Factor)是哈希表性能的核心指标,定义为 α = n / m(元素数 / 桶数)。合理设置负载因子阈值并在超过时触发扩容,是哈希表实现中的关键调优点。

主流语言的负载因子阈值

  • Python dict: 大约 2/3(约 0.667),偏向时间效率,但会消耗更多内存
  • Java HashMap: 0.75,经验表明在时间和空间之间取得良好平衡
  • C++ unordered_map: 1.0(链地址法),利用链表处理更高负载
  • Go map: 6.5(基于桶的链地址法,每桶 8 个槽位),极高内存效率
import time import random import sys def benchmark_dict(n): """测量字典在不同负载下的性能表现""" d = {} # 预插入数据 for i in range(n): d[i] = i # 测试随机查找性能 keys = [random.randint(0, n - 1) for _ in range(10000)] start = time.perf_counter() for k in keys: _ = d[k] elapsed = time.perf_counter() - start return elapsed, sys.getsizeof(d) # 测试不同规模下的性能 for n in [100, 1000, 10000, 100000, 1000000]: elapsed, mem = benchmark_dict(n) load = n / (2 ** (n.bit_length())) # 估算负载因子 print(f"n={n:<8} 查找10000次={elapsed:.4f}s " f"内存={mem/1024:.1f}KB 负载≈{load:.3f}")

哈希表性能调优最佳实践

  1. 初始容量预估: 如果已知数据规模,初始化时指定容量可以避免多次扩容开销。Python 中 dict.fromkeys(keys) 比逐次插入更快
  2. 选择高效的哈希函数: 计算速度与分布均匀性需要权衡。密码学哈希函数(如 SHA-256)虽然均匀但不适合普通哈希表
  3. 避免哈希碰撞攻击: 使用随机种子(Python 3.3+ 默认启动 PYTHONHASHSEED 随机化)防止恶意构造全碰撞数据
  4. 合理选择扩容因子: 常见的扩容倍数是 2x,但更大的倍数(如 4x)可以减少扩容次数但浪费更多内存
  5. 内存布局优化: 紧凑表示法(如 Python 3.6+)或开放寻址法可以减少指针开销,提高缓存命中率

在性能敏感的场景中,选择合适的哈希表实现可以带来数量级的性能提升。例如,对于小规模数据(几十个元素),线性搜索数组可能比哈希表更快(因为哈希计算和间接寻址的开销超过了线性扫描)。对于中等规模数据,Google 的 SwissTable(C++ absl::flat_hash_map)利用 SIMD 指令并行探测多个槽位,在大数据量下比传统哈希表快数倍。

八、核心要点总结

九、进一步思考

哈希表虽然是一套经典的数据结构,但在实际工程中仍有许多值得深入思考的方向:

工程实践中的哈希表设计考量

  • 持久化哈希表: 如果哈希表需要写入磁盘或持久化存储,如何处理扩容时的数据一致性?LevelDB 和 RocksDB 中的哈希结构如何保证 crash-safety?
  • 并发安全: 多线程场景下如何实现高效的并发哈希表?细粒度锁(分段锁)、无锁哈希表(CAS 操作)、读写分离是三种常见技术路线
  • 哈希表 vs 跳表: Redis 为什么同时使用哈希表和跳表实现有序集合(ZSET)?两者在范围查询、内存开销、实现复杂度上的区别是什么?
  • 哈希表的缓存性能: 为什么 Google 的 SwissTable 比传统开放寻址哈希表快?SIMD 指令如何优化探测过程?内存带宽和延迟对哈希表性能有多大影响?
  • "完美哈希": 在静态键集合上(如编译器的关键字集合),是否可以构造一个没有冲突的完美哈希函数?最小的完美哈希函数空间是多少?

理解哈希表不仅仅是记住它能在 O(1) 时间内查找键值对——更要理解这个 O(1) 背后的假设(哈希函数均匀、负载因子合理、扩容代价均摊),以及在假设不成立时系统如何优雅降级。数据结构没有银弹,只有权衡。

建议读者进一步阅读以下资料来深化理解:CLRS《算法导论》第11章(哈希表理论)、CPython 源码中的 Objects/dictobject.c(Python 字典实现)、Google 的 Abseil 库中的 SwissTable 实现,以及 Redis 源码中的哈希表渐进式 rehash 实现。