本文围绕哈希冲突的解决,详细介绍了开放定址法和链地址法两种核心方案,包括各自的命名空间设计、哈希函数(含字符串 BKDR 算法)、节点与哈希表结构定义,以及 find、insert、erase 等关键操作的实现逻辑,还涉及负载因子阈值触发的扩容与重哈希机制。
实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。
HashTable 命名冲突,我们将实现的闭散列(开放定址法)哈希表放置在 open_address 命名空间中。
size_t 类型。
//处理int double char flot
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return key;
}
};std::string 提供一个专用版本,采用 BKDR 哈希算法 逐个字符计算出一个分布均匀的 size_t 哈希值。//处理string
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string str)
{
size_t ret = 0;
for (size_t i = 0; i < str.size(); i++)
{
ret *= 131;
ret += str[i];
}
return ret;
}
};
这样,在 HashTable 的模板参数中,我们可以提供一个默认的 HashFunc 参数(即 DefaultHashFunc<K>),编译器会根据关键码 K 的实际类型自动选择最匹配的仿函数版本。
每个哈希节点需要存储两个核心信息:
kv(类型为 std::pair<K, V>)。EMPTY:空,从未使用过或已被删除。EXIST:存在,当前存储着一个有效的键值对。DELETE:已删除,此位置曾被占用但内容已删。在哈希表初始化时,所有节点的状态都应被设置为 EMPTY。
enum State
{
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State _state = EMPTY;
};哈希表 (HashTable) 的成员变量:
std::vector<HashNode<K, V>> _table HashNode。size_t _n EXIST 的节点数量(即存储了多少个有效键值对)。load_factor = _n / _table.size()。当负载因子超过阈值时,触发扩容操作。构造函数与初始化:
在构造函数中,我们需要为 _table 预分配空间。这里有一个关键细节:
vector::reserve(),因为它只增加 capacity,不改变 size。这意味着 operator[] 仍然是不可用的,会导致越界访问。vector::resize()。例如,_table.resize(10) 会真正创建10个可被直接访问的 HashNode 对象,并将每个节点的状态初始化为 EMPTY,同时 _n 设置为0。//哈希表设计
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
private:
vector<HashNode<K, V>> _table;
size_t _n = 0;
};#pragma once
#include<iostream>
#include<vector>
using namespace std;
//处理int double char flot
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
//处理string
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string str)
{
size_t ret = 0;
for (size_t i = 0; i < str.size(); i++)
{
ret *= 131;
ret += str[i];
}
return ret;
}
};
namespace open_adress
{ //枚举节点的状态
enum State
{
EXIST,
EMPTY,
DELETE
};
//节点设计
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State _state = EMPTY;//初始化为空
};
//哈希表设计
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
private:
vector<HashNode<K, V>> _table;
size_t _n = 0;
};
}
// 查找函数
HashNode<K, V>* Find(const K& key)
{
HashFunc hashFunc; // 哈希函数对象
// 计算初始哈希位置
size_t hashi = hashFunc(key) % _table.size();
size_t start = hashi; // 记录起始位置,用于检测是否遍历完整个表
// 线性探测查找
while (_table[hashi]._state != EMPTY)
{
// 如果当前位置状态为EXIST且key匹配,则找到目标
if (_table[hashi]._state == EXIST &&
_table[hashi]._kv.first == key)
{
return &_table[hashi];// 只返回地址,无拷贝开销,并且可以直接修改
}
// 继续向后探测
hashi = (hashi + 1) % _table.size();
// 如果回到起始位置,说明遍历完整个表都没找到
if (hashi == start)
{
break;
}
}
// 没找到,返回空指针
return nullptr;
}hashi。这个位置是元素理论上应该存储的理想地址。我们检查该位置节点的状态:如果状态为 EXIST,说明发生了哈希冲突,即不同关键码映射到了同一位置。
hashi = (hashi + 1) % _table.size() 的方式迭代遍历,这样既能确保索引在哈希表范围内循环不越界,又能充分利用哈希表的空间。由于哈希表在设计时会通过扩容机制保证总有空闲位置,我们可以确信在持续查找过程中一定能找到可用的插入位置。
EMPTY 或 DELETE 的节点。这两种状态都表示该位置可供新元素使用。一旦找到这样的位置,我们将键值对数据存入节点的 _kv 字段,将其状态标记为 EXIST,同时将有效元素计数器 _n 加1,完成整个插入操作。

_n 与哈希表总大小 _table.size() 的比值。
0.7)时,说明哈希表已经过于拥挤,查找效率会显著下降。此时我们创建一个新的临时哈希表,其容量通常是原表的两倍左右(选择质数容量有助于减少哈希冲突)。然后遍历原哈希表中的所有有效元素(状态为 EXIST 的节点),对每个元素重新计算其在新表中的哈希位置并插入。这个过程称为"重哈希"。

bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first)) // 说明这个值已经存在,返回错误
{
return false;
}
// 当负载因子>=0.7的时候进行扩容
if ((double)_n / _table.size() >= 0.7)
{
size_t newSize = _table.size() * 2;
// 创建新表
vector<HashNode<K, V>> newTable;
newTable.resize(newSize);
// 重新哈希所有元素到新表
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXIST)
{
// 计算在新表中的位置
size_t hashi = hf(_table[i]._kv.first) % newSize;
// 在新表中线性探测找到空位
while (newTable[hashi]._state == EXIST)
{
hashi = (hashi + 1) % newSize;
}
// 直接插入到新表
newTable[hashi]._kv = _table[i]._kv;
newTable[hashi]._state = EXIST;
}
}
// 交换表
_table.swap(newTable);
}
// 计算插入位置
size_t hashi = hf(kv.first) % _table.size();
// 线性探测找到空位
while (_table[hashi]._state == EXIST)
{
hashi++;
hashi %= _table.size();
}
// 插入新元素
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
_n++;
return true;
}bool Erase(const K& key)
{
// 先查找要删除的元素
HashNode<K, V>* node = Find(key);
if (node == nullptr)
{
return false; // 元素不存在,删除失败
}
// 将状态标记为DELETE,但保留数据(伪删除)
node->_state = DELETE;
_n--; // 有效元素数量减1
return true;
}
开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。

HashTable 命名冲突,我们将实现的**链地址法(哈希桶)**放置在 hash_bucket 命名空间中。
size_t 类型。
这里的“转换为 size_t 类型”仿函数和上述开放地址法是共用的,这里就不多解释了。每个哈希节点需要存储两个核心信息:
kv(类型为 std::pair<K, V>)。next,用于构建链表结构解决哈希冲突。在哈希表初始化时,所有桶的位置都应被设置为
nullptr,表示空的链表头。 与开放定址法不同,哈希桶不需要显式的状态标记,状态通过链表结构隐式管理:
_table[i] == nullptr,表示该位置没有元素_table[i] != nullptr,指向一个链表,包含所有哈希到该位置的元素 //节点设计
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(pair<K, V>& kv)
:ka(_kv)
,_next(nullptr)
{ }
};哈希表 (HashTable) 的成员变量:
std::vector<HashNode<K, V>*> _table size_t _n load_factor = _n / _table.size()。当负载因子超过阈值时,触发扩容操作。构造函数与初始化:
在构造函数中,我们需要为 _table 预分配空间。这里有一个关键细节:
vector::reserve(),因为它只增加 capacity,不改变 size。这意味着 operator[] 仍然是不可用的,会导致越界访问。vector::resize()。例如,_table.resize(10, nullptr) 会真正创建10个可被直接访问的指针位置,并将每个指针初始化为 nullptr,表示空的链表头,同时 _n 设置为0。// 哈希表设计 - 哈希桶实现
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10, nullptr); // 创建10个桶,全部初始化为空链表
}
private:
vector<HashNode<K, V>*> _table; // 指针数组,每个位置指向一个链表头
size_t _n = 0; // 存储的有效元素个数
};#pragma once
#include<iostream>
#include<vector>
using namespace std;
//处理int double char flot
template<class K>
struct DefaultHashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
//处理string
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string str)
{
size_t ret = 0;
for (size_t i = 0; i < str.size(); i++)
{
ret *= 131;
ret += str[i];
}
return ret;
}
};
namespace hash_bucket
{
//节点设计
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 HashFunc = DefaultHashFunc<K>>//三个默认模板参数
class HashTable
{
public:
HashTable()
{
_table.resize(10,nullptr);
}
private:
vector<HashNode<K, V>*> _table;
size_t _n = 0;
};
}nullptr),则表明目标元素不存在于表中;哈希桶法的查找过程直接利用链表结构解决冲突,无需复杂的线性探测,通过简单的链表遍历即可完成精确查找。

HashNode<K, V>* Find(const K& key)
{
HashFunc hf; // 哈希函数对象
// 计算初始哈希位置
size_t hashi = hf(key) % _table.size();
HashNode<K, V>* cur = _table[hashi];//遍历这个桶的链表
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
// 继续向后探测
cur = cur->_next;
}
// 没找到,返回空指针
return nullptr;
}hashi。这个位置确定了元素应该存储在哪个桶对应的链表中。
next 指针指向当前链表的头节点,然后更新桶的头指针指向新节点。这种插入方式简单高效,时间复杂度为 O(1)。插入完成后,将有效元素计数器 _n 加 1。

_n 与哈希表桶数量 _table.size() 的比值。
1.0)时,说明平均每个桶的链表长度过长,查找效率会下降。此时我们创建一个新的临时哈希表,其容量通常是原表的两倍左右。选择质数容量有助于减少哈希冲突,使元素分布更加均匀。
负载因子:计算公式为当前有效元素个数
_n与哈希桶总大小_table.size()的比值
扩容操作和链式地址法类似,这里不画图理理解了
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first)) // 说明这个值已经存在,返回错误
{
return false;
}
// 当负载因子>=2.0的时候进行扩容
if ((double)_n / _table.size() >= 1.0)
{
size_t newSize = _table.size() * 2;
// 创建新表
vector<HashNode<K, V>*> newTable;
newTable.resize(newSize, nullptr);
// 重新哈希所有元素到新表
for (size_t i = 0; i < _table.size(); i++)
{
HashNode<K, V>* cur = _table[i];
while (cur)
{
HashNode<K, V>* next = cur->_next;
size_t hash = hf(cur->_kv.first) % newSize;
// 头插法插入到新表
cur->_next = newTable[hash];
newTable[hash] = cur;
cur = next;
}
_table[i] = nullptr; // 原表置空
}
// 交换表
_table.swap(newTable);
}
// 计算插入位置
size_t hash = hf(kv.first) % _table.size();
// 头插法插入新节点
HashNode<K, V>* newNode = new HashNode<K, V>(kv);
newNode->_next = _table[hash];
_table[hash] = newNode;
_n++;
return true;
}
bool Erase(const K& key)
{
HashFunc hf;
// 计算桶位置
size_t hash = hf(key) % _table.size();
HashNode<K, V>* cur = _table[hash];
HashNode<K, V>* prev = nullptr;
// 遍历链表查找要删除的节点
while (cur)
{
if (cur->_kv.first == key)
{
// 找到要删除的节点
if (prev == nullptr)
{
// 要删除的是头节点
_table[hash] = cur->_next;
}
else
{
// 要删除的是中间或尾节点
prev->_next = cur->_next;
}
// 释放节点内存
delete cur;
_n--; // 有效元素数量减1
return true;
}
prev = cur;
cur = cur->_next;
}
return false; // 元素不存在,删除失败
}在哈希桶法的实现中,由于存储结构使用指针数组管理动态分配的链表节点,我们必须显式编写析构函数来手动释放内存,否则会造成严重的内存泄漏问题。这种必要性源于哈希桶的特殊内存布局:vector容器仅负责管理指针数组本身的空间,而每个指针所指向的链表节点都是在堆上动态分配的独立内存块。当哈希表对象生命周期结束时,vector的析构函数会自动释放指针数组占用的内存,但对于数组中每个指针所指向的链表节点内存,系统无法自动识别和释放。
析构过程需要遍历哈希表中的每个桶,对每个非空桶对应的链表进行完整的节点遍历和释放。在释放每个节点时,必须采用"先保存后释放"的策略,即在删除当前节点之前,预先保存其下一个节点的地址,这样才能保证链表遍历的连续性。整个释放过程从链表的头节点开始,依次向后推进,直到所有节点都被安全释放,最后将桶的指针置为空值,完成整个哈希表的内存清理工作。这种手动内存管理机制确保了动态分配的所有链表节点都能被正确回收,维护了程序的内存安全。
~HashTable()
{
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while(cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
#pragma once
#include<iostream>
#include<vector>
using namespace std;
//仿函数 处理int double char flot等
template<class K>
struct DefaultHashFunc// DefaultHashFunc: 默认哈希函数
{
size_t operator()(const K& key)
{
return key;
}
};
//处理string
template<>
struct DefaultHashFunc<string>
{
size_t operator()(const string str)
{
size_t ret = 0;
for (size_t i = 0; i < str.size(); i++)
{
ret *= 131;
ret += str[i];
}
return ret;
}
};
namespace open_address
{
//枚举节点的状态
enum State
{
EXIST,
EMPTY,
DELETE
};
//节点设计
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State _state = EMPTY;//初始化为空
};
//哈希表设计
template<class K, class V, class HashFunc = DefaultHashFunc<K>>//三个默认模板参数
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
// 查找函数
HashNode<K, V>* Find(const K& key)
{
HashFunc hf; // 哈希函数对象
// 计算初始哈希位置
size_t hashi = hf(key) % _table.size();
size_t start = hashi; // 记录起始位置,用于检测是否遍历完整个表
// 线性探测查找
while (_table[hashi]._state != EMPTY)
{
// 如果当前位置状态为EXIST且key匹配,则找到目标
if (_table[hashi]._state == EXIST &&
_table[hashi]._kv.first == key)
{
return &_table[hashi];// 只返回地址,无拷贝开销,并且可以直接修改
}
// 继续向后探测
hashi = (hashi + 1) % _table.size();
// 如果回到起始位置,说明遍历完整个表都没找到
if (hashi == start)
{
break;
}
}
// 没找到,返回空指针
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first)) // 说明这个值已经存在,返回错误
{
return false;
}
// 当负载因子>=0.7的时候进行扩容
if ((double)_n / _table.size() >= 0.7)
{
size_t newSize = _table.size() * 2;
// 创建新表
vector<HashNode<K, V>> newTable;
newTable.resize(newSize);
// 重新哈希所有元素到新表
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXIST)
{
// 计算在新表中的位置
size_t hashi = hf(_table[i]._kv.first) % newSize;
// 在新表中线性探测找到空位
while (newTable[hashi]._state == EXIST)
{
hashi = (hashi + 1) % newSize;
}
// 直接插入到新表
newTable[hashi]._kv = _table[i]._kv;
newTable[hashi]._state = EXIST;
}
}
// 交换表
_table.swap(newTable);
}
// 计算插入位置
size_t hashi = hf(kv.first) % _table.size();
// 线性探测找到空位
while (_table[hashi]._state == EXIST)
{
hashi++;
hashi %= _table.size();
}
// 插入新元素
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
_n++;
return true;
}
bool Erase(const K& key)
{
// 先查找要删除的元素
HashNode<K, V>* node = Find(key);
if (node == nullptr)
{
return false; // 元素不存在,删除失败
}
// 将状态标记为DELETE,但保留数据(伪删除)
node->_state = DELETE;
_n--; // 有效元素数量减1
return true;
}
private:
vector<HashNode<K, V>> _table;
size_t _n = 0;
};
}
namespace hash_bucket
{
//节点设计
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 HashFunc = DefaultHashFunc<K>>//三个默认模板参数
class HashTable
{
public:
HashTable()
{
_table.resize(10,nullptr);
}
~HashTable()
{
for (int i = 0; i < _table.size(); i++)
{
HashNode<K, V>* cur = _table[i];
while (cur)
{
HashNode<K,V>* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
// 查找函数
HashNode<K, V>* Find(const K& key)
{
HashFunc hf; // 哈希函数对象
// 计算初始哈希位置
size_t hashi = hf(key) % _table.size();
HashNode<K, V>* cur = _table[hashi];//遍历这个桶的链表
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
// 继续向后探测
cur = cur->_next;
}
// 没找到,返回空指针
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
if (Find(kv.first)) // 说明这个值已经存在,返回错误
{
return false;
}
// 当负载因子>=1.0的时候进行扩容
if ((double)_n / _table.size() >= 1.0)
{
size_t newSize = _table.size() * 2;
// 创建新表
vector<HashNode<K, V>*> newTable;
newTable.resize(newSize, nullptr);
// 重新哈希所有元素到新表
for (size_t i = 0; i < _table.size(); i++)
{
HashNode<K, V>* cur = _table[i];
while (cur)
{
HashNode<K, V>* next = cur->_next;
size_t hash = hf(cur->_kv.first) % newSize;
// 头插法插入到新表
cur->_next = newTable[hash];
newTable[hash] = cur;
cur = next;
}
_table[i] = nullptr; // 原表置空
}
// 交换表
_table.swap(newTable);
}
// 计算插入位置
size_t hash = hf(kv.first) % _table.size();
// 头插法插入新节点
HashNode<K, V>* newNode = new HashNode<K, V>(kv);
newNode->_next = _table[hash];
_table[hash] = newNode;
_n++;
return true;
}
bool Erase(const K& key)
{
HashFunc hf;
// 计算桶位置
size_t hash = hf(key) % _table.size();
HashNode<K, V>* cur = _table[hash];
HashNode<K, V>* prev = nullptr;
// 遍历链表查找要删除的节点
while (cur)
{
if (cur->_kv.first == key)
{
// 找到要删除的节点
if (prev == nullptr)
{
// 要删除的是头节点
_table[hash] = cur->_next;
}
else
{
// 要删除的是中间或尾节点
prev->_next = cur->_next;
}
// 释放节点内存
delete cur;
_n--; // 有效元素数量减1
return true;
}
prev = cur;
cur = cur->_next;
}
return false; // 元素不存在,删除失败
}
private:
vector<HashNode<K, V>*> _table;
size_t _n = 0;
};
}
//int main()
//{
// open_address::HashTable<string, string> ht;
//
// ht.Insert(make_pair("sort", "排序"));
// ht.Insert(make_pair("insert", "xxx"));
//
// open_address::HashNode< string, string>* ret = ht.Find("insert");
//
// ret->_kv.second = "插入";
//
//
// return 0;
//}
//int main()
//{
// hash_bucket::HashTable<int, int> ht;
//
// ht.Insert(make_pair(12, 21));
// ht.Insert(make_pair(111, 111));
// ht.Insert(make_pair(45, 58));
// ht.Insert(make_pair(15, 15));
// ht.Insert(make_pair(93, 3));
// ht.Insert(make_pair(493, 43));
// ht.Insert(make_pair(1933, 133));
// ht.Insert(make_pair(5673, 53));
// ht.Insert(make_pair(132, 132));
//
// cout << ht.Erase(12) << endl;
// cout << ht.Erase(1933) << endl;
//
// cout << ht.Find(12) << endl;
// cout << ht.Find(45) << endl;
// cout << ht.Find(5673) << endl;
//
// return 0;
//}开放定址法通过数组存储元素、状态标记和线性探测处理冲突,链地址法借助链表链接冲突元素、物理增删节点,两种方法分别适配不同场景,其核心均是通过合理的结构设计与操作逻辑,平衡哈希表的查找效率、空间利用率与内存管理安全性。