好的,我们来详细讲解C++中哈希表的两种常见实现方式:开散列 (Open Hashing / Separate Chaining)和闭散列(Closed Hashing / Open Addressing)。
哈希表基础
哈希表通过哈希函数将键映射到数组中的索引位置。理想情况下,每个键映射到唯一索引,但不同键可能映射到相同位置,称为哈希冲突。解决冲突是哈希表实现的关键。
1. 闭散列(Open Addressing)
闭散列中,所有元素都存储在数组本身中。发生冲突时,按照特定规则(探测序列)寻找下一个空槽位。
常见探测方法
- 线性探测 :冲突后,顺序检查下一个槽位(索引+1)。
- 公式:h_i(k) = (h(k) + i) \\mod N,其中 N 为表大小。
- 二次探测 :避免线性探测的聚集问题,探测步长为平方。
- 公式:h_i(k) = (h(k) + c_1 i + c_2 i\^2) \\mod N。
- 双重哈希 :使用第二个哈希函数计算步长。
- 公式:h_i(k) = (h_1(k) + i \\cdot h_2(k)) \\mod N。
特点
- 优点 :
- 内存连续,缓存友好。
- 无需额外链表结构。
- 缺点 :
- 装载因子(\\alpha = \\frac{\\text{元素数}}{\\text{表大小}})需严格控制(通常 \\alpha \< 0.7),否则性能下降。
- 删除操作复杂(需标记为"已删除"而非直接清空)。
示例代码(线性探测)
cpp
class HashTable {
private:
vector<int> table; // 存储数据
vector<bool> deleted; // 标记删除状态
size_t capacity;
size_t size = 0;
size_t hash(int key) {
return key % capacity;
}
public:
HashTable(size_t cap) : capacity(cap) {
table.resize(cap, -1); // -1表示空槽
deleted.resize(cap, false);
}
bool insert(int key) {
if (size >= capacity) return false;
size_t idx = hash(key);
while (table[idx] != -1 && !deleted[idx]) { // 寻找空槽或已删除槽
if (table[idx] == key) return false; // 键已存在
idx = (idx + 1) % capacity;
}
table[idx] = key;
deleted[idx] = false;
size++;
return true;
}
bool find(int key) {
size_t idx = hash(key);
size_t start = idx;
do {
if (table[idx] == key && !deleted[idx]) return true;
if (table[idx] == -1 && !deleted[idx]) break; // 遇到未删除的空槽
idx = (idx + 1) % capacity;
} while (idx != start);
return false;
}
bool erase(int key) {
size_t idx = hash(key);
size_t start = idx;
do {
if (table[idx] == key && !deleted[idx]) {
deleted[idx] = true; // 标记删除
size--;
return true;
}
if (table[idx] == -1 && !deleted[idx]) break;
idx = (idx + 1) % capacity;
} while (idx != start);
return false;
}
};
2. 开散列(Separate Chaining)
开散列中,每个数组槽位是一个链表(或其它容器)。冲突时,元素直接添加到对应链表中。
特点
- 优点 :
- 装载因子可更高(\\alpha \> 1 仍有效)。
- 删除操作简单(直接移除链表节点)。
- 缺点 :
- 指针开销大,内存碎片化。
- 缓存不友好(链表节点可能分散)。
示例代码
cpp
class HashTable {
private:
vector<list<int>> buckets; // 每个桶是一个链表
size_t capacity;
size_t hash(int key) {
return key % capacity;
}
public:
HashTable(size_t cap) : capacity(cap) {
buckets.resize(cap);
}
bool insert(int key) {
size_t idx = hash(key);
for (auto it = buckets[idx].begin(); it != buckets[idx].end(); ++it) {
if (*it == key) return false; // 键已存在
}
buckets[idx].push_back(key);
return true;
}
bool find(int key) {
size_t idx = hash(key);
for (int val : buckets[idx]) {
if (val == key) return true;
}
return false;
}
bool erase(int key) {
size_t idx = hash(key);
for (auto it = buckets[idx].begin(); it != buckets[idx].end(); ++it) {
if (*it == key) {
buckets[idx].erase(it);
return true;
}
}
return false;
}
};
对比与选择
| 特性 | 闭散列 | 开散列 |
|---|---|---|
| 内存布局 | 连续数组 | 数组+链表/树 |
| 装载因子限制 | 较低(通常 \\alpha \< 0.7) | 较高(可 \\alpha \> 1) |
| 删除复杂度 | 需特殊标记 | 直接删除节点 |
| 缓存友好度 | 高 | 低 |
| 实现难度 | 中等(需处理探测序列) | 简单 |
适用场景
- 闭散列:对缓存性能要求高、内存受限的场景(如嵌入式系统)。
- 开散列 :通用场景,标准库(如
std::unordered_map)常用此实现。
总结
- 闭散列通过探测序列解决冲突,适合内存紧凑的场景。
- 开散列通过链表解决冲突,扩展性强,易于实现。
- 实际选择需权衡内存效率 、性能需求 和实现复杂度。