C++链式哈希表怎么解决冲突?拉链法实现原理详解
作者:Brilliantwxx
链式哈希表(Chaining Hash Table / 拉链法哈希表)
链式哈希表是最经典、工业界应用最广的哈希表实现方案之一,核心作用是解决哈希冲突问题:当不同的Key经过哈希函数计算后,映射到同一个桶位置时,用「链表挂载」的方式把所有冲突的键值对挂在同一个桶下,避免数据覆盖。
一、底层结构组成
它的整体结构是 「固定大小的桶数组 + 每个桶挂载的动态链表」 的两层设计:
组成部分 | 说明 |
|---|---|
桶数组(Slot Array) | 底层是一个连续内存的数组,长度为 m(也叫槽数/容量),数组的每个位置称为一个「桶」,初始状态所有桶都是空指针 |
桶挂载的链表 | 每个桶不直接存键值对,而是作为链表的头节点,所有经过哈希计算落到该桶的 (Key, Value)对,都以链表节点的形式追加到这个链表中 |
负载因子 α | 核心调控指标:α = 已存储元素个数 n / 桶数量 m,用来衡量哈希表的拥挤程度,也是触发扩容的核心判断条件 |
二、代码结构与核心定义
我们先来看一下代码的基础骨架。
1. 节点结构 (HashNode)
template<class K, class V>
struct HashNode
{
pair<K, V> _kv; // 存储键值对
HashNode<K, V>* _next; // 指向下一个冲突节点
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};每个节点存储一个 pair<K, V>和一个指向下一个节点的指针。这是一个典型的单链表节点。
2. 哈希表主体 (HashTable)
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _tables; // 桶数组
size_t _n = 0; // 表中实际元素个数
};这里使用了 vector<Node*>作为桶数组,而不是 vector<list<pair<K,V>>>。这样做的好处是减少了 list 容器的额外开销,更加贴近底层实现。
三、关键算法详解
1. 哈希函数与取模
为了将 Key 映射到桶下标,我们需要两步:
- 将 Key 转换为整数(
HashFunc)。 - 对桶大小取模(
%)。
Hash hs; size_t hashi = hs(key) % _tables.size();
这里的 Hash是一个仿函数(Functor),允许用户自定义哈希规则。
2. 插入操作 (Insert)
插入逻辑分为三步:检查重复 -> 检查扩容 -> 头插。
负载因子与扩容策略
这是哈希表性能的精髓。代码中设定了独特的扩容阈值:

bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
Hash hs;
// 负载因子==1扩容
if (_n == _tables.size())
{
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
// 当前桶的节点重新映射挂到新表
while (cur)
{
Node* next = cur->_next;
// 插入到新表
size_t hashi = hs(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hashi = hs(kv.first) % _tables.size();
// 头插
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_n;
return true;
}解析:
- 负载因子 (Load Factor) α = 元素个数 / 桶个数 (α=sizen)。
- 大多数语言(如 Java)默认阈值是 0.75,但这份代码采用了 1.0。
- 为什么是 1.0? 因为底层使用了质数作为桶大小(见下文),配合质数取模,冲突分布相对均匀,允许更高的负载因子而不至于性能急剧下降。
Rehash(重哈希)
扩容时,桶的大小变为原来的若干倍(通常是下一个质数),并遍历旧表将所有节点重新计算位置插入新表。
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next; //保存下一个链节点的地址,防止断链
size_t hashi = hs(cur->_kv.first) % newtables.size();//找到新表的桶位置
// 头插到新表桶
cur->_next = newtables[hashi]; //将当前链节点next指针指向新表桶位置,等于接上链
newtables[hashi] = cur; //将接上后的链再接入表节点
cur = next; //指针往后走,继续断链接链
}
_tables[i] = nullptr;//修改完成,原表置空
}
_tables.swap(newtables);注意细节:这里在搬运节点时,并没有重新 new节点,而是直接改变了节点的 _next指针指向,这是一种高效的“指针窃取”操作。
3. 查找与删除
查找和删除都需要先定位桶,然后遍历链表。
查找 (Find)
Node* Find(const K& key)
{
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];//定义链指针
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}删除 (Erase)
删除单链表节点需要处理两种情况:删除头节点和删除中间节点。
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr) // 头删
_tables[hashi] = cur->_next;
else // 中间删
prev->_next = cur->_next;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}完整代码
namespace SeparateChaining
{
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
:_tables(__stl_next_prime(1), nullptr)
, _n(0)
{
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
// 当前桶的节点重新映射挂到新表
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
Hash hs;
// 负载因子==1扩容
if (_n == _tables.size())
{
vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
// 当前桶的节点重新映射挂到新表
while (cur)
{
Node* next = cur->_next;
// 插入到新表
size_t hashi = hs(cur->_kv.first) % newtables.size();
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hashi = hs(kv.first) % _tables.size();
// 头插
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_n;
return true;
}
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
//vector<list<pair<K, V>>> _tables;
vector<Node*> _tables;
size_t _n = 0; // 实际存储的数据个数
};
}总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
