前言
哈希表是数据结构中的核心组件,而**哈希桶(开散列)**则是解决哈希冲突最实用的方法之一。本文将深入剖析一份完整的哈希桶实现代码,从节点定义到迭代器实现,再到核心操作的源码级解读。
一、整体架构概览
在深入代码之前,我们先了解整个哈希桶的架构设计:
整个实现包含四个核心部分:
HashNode:哈希桶的节点定义 HashFunc:哈希仿函数(支持自定义类型的哈希转换) __HashIterator:迭代器实现(支持范围 for 循环) HashTable:主体实现(包含所有核心操作)
哈希表作为数据结构核心组件,哈希桶(开散列)利用数组结合单链表有效解决哈希冲突。内容涵盖 HashNode 节点定义、HashFunc 仿函数设计、迭代器跳转逻辑及 HashTable 主体架构。重点解析了插入操作的负载因子控制与素数扩容机制,采用零拷贝方式迁移节点以提升效率。同时阐述了查找与删除操作中前驱指针维护及头尾节点处理细节,展示了基于 KeyOfT 泛型萃取实现 set 与 map 底层复用的设计思路。
哈希表是数据结构中的核心组件,而**哈希桶(开散列)**则是解决哈希冲突最实用的方法之一。本文将深入剖析一份完整的哈希桶实现代码,从节点定义到迭代器实现,再到核心操作的源码级解读。
在深入代码之前,我们先了解整个哈希桶的架构设计:
整个实现包含四个核心部分:
HashNode:哈希桶的节点定义 HashFunc:哈希仿函数(支持自定义类型的哈希转换) __HashIterator:迭代器实现(支持范围 for 循环) HashTable:主体实现(包含所有核心操作)
template<class T>
struct HashNode {
T _data; // 存储的数据
HashNode<T>* _next; // 指向下一个节点的指针
HashNode(const T& data) : _data(data), _next(nullptr) {}
};
设计思路:
单向链表结构:每个桶本质上就是一个不带头结点的单链表。 终极泛型设计
T:注意这里存的不是Key和Value,而是泛型T。当封装set时T就是Key;封装map时T就是pair<const Key, Value>。这是 STL 复用代码的精髓。
template<class K>
struct HashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
// 字符串特化版本 (BKDR 哈希算法)
template<>
struct HashFunc<string> {
size_t operator()(const string& key) {
size_t val = 0;
for (auto ch : key) {
val *= 131; // 131 是经验值,有效降低冲突
val += ch;
}
return val;
}
};
核心思维:
仿函数:重载
operator(),使对象可以像函数一样调用。它负责将各种千奇百怪的 Key 统一转化为size_t,以便进行后续的取模运算。 模板特化:针对string提供专门的哈希算法(BKDR Hash),这也是面试中常考的字符串哈希处理方式。
迭代器是哈希桶最精妙的设计之一,它需要解决一个核心问题:如何在不同的桶之间跳转?
template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator {
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef __HashIterator<K, T, Hash, KeyOfT> Self;
Node* _node; // 当前指向的节点
HT* _pht; // 指向哈希表的指针(关键!)
__HashIterator(Node* node, HT* pht) : _node(node), _pht(pht) {}
};
因为迭代器一旦发现当前节点的 _next 为空,就需要去哈希表的底层数组 _tables 中向后寻找下一个非空的桶。没有 _pht 指针,迭代器就像无头苍蝇,根本不知道数组在哪里。
在当前桶的链表上移动(通过
_node->_next) 当当前桶遍历完,跳到下一个非空桶(需要访问_pht->_tables数组)
Self& operator++() {
if (_node->_next) {
// 情况 1:当前桶的链表还没走完,直接走向下一个节点
_node = _node->_next;
} else {
// 情况 2:当前桶走完了,去数组里找下一个非空的桶
Hash hash;
KeyOfT kot;
// 难点:如何知道当前是在几号桶?
// 只能通过当前节点的数据重新计算一次哈希值来定位!
size_t i = hash(kot(_node->_data)) % _pht->_tables.size();
++i; // 从下一个桶开始找
// 遍历数组找非空桶
for (; i < _pht->_tables.size(); ++i) {
if (_pht->_tables[i]) {
_node = _pht->_tables[i];
return *this;
}
}
// 找遍了也没找到,说明遍历彻底结束,指向 end()
_node = nullptr;
}
return *this;
}
通过**
_pht指针**访问哈希表内部结构 使用KeyOfT从数据中提取键值 遍历结束返回nullptr代表end()
vector<Node*> _tables; // 指针数组,每个元素是链表的头指针
size_t _size = 0; // 存储的有效元素个数
iterator begin() {
for (size_t i = 0; i < _tables.size(); i++) {
if (_tables[i]) {
return iterator(_tables[i], this);
}
}
return end();
}
iterator end() {
return iterator(nullptr, this);
}
注意:begin()找到第一个非空桶的第一个节点,这符合哈希表无序的特性。
pair<iterator, bool> Insert(const T& data) {
Hash hash;
KeyOfT kot;
// 步骤 1:去重检查 (利用 KeyOfT 提取 Key 进行查找)
iterator ret = Find(kot(data));
if (ret != end()) {
return make_pair(ret, false); // 已经存在,插入失败
}
// 步骤 2:负载因子控制与极速扩容
if (_size == _tables.size()) {
// 负载因子达到 1 时扩容
vector<Node*> newTables;
// 获取比当前容量大的下一个质数作为新容量 (质数能有效降低哈希冲突)
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 核心优化:直接迁移旧节点,不进行任何 new 和 delete
for (size_t i = 0; i < _tables.size(); ++i) {
Node* cur = _tables[i];
while (cur) {
Node* next = cur->_next; // 提前保存下一个节点
// 重新计算在新表中的位置
size_t hashi = hash(kot(cur->_data)) % newTables.size();
// 头插法接入新表
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables); // 瞬间接管新表
}
// 步骤 3:计算哈希位置并头插新节点
size_t hashi = hash(kot(data)) % _tables.size();
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
if (_size == _tables.size()) { // 负载因子 == 1
vector<Node*> newTables;
// 获取下一个质数作为新容量
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 重新映射所有节点
for (size_t i = 0; i < _tables.size(); i++) {
Node* cur = _tables[i];
while (cur) {
Node* next = cur->_next;
// 重新计算在新表中的位置
size_t hashi = hash(kot(cur->_data)) % newTables.size();
// 头插到新表
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
扩容核心思维:
何时扩容:当有效元素个数等于桶的个数时(负载因子=1),此时平均每个桶挂一个节点,是扩容的黄金时机。 容量选择:通过打表法(
__stl_next_prime),每次扩容都选择一个接近两倍的质数。 节点零拷贝迁移:绝对不要new新节点,而是直接将旧表中的节点指针'摘下来',重新计算位置后挂到新表上,效率拉满!
iterator Find(const K& key) {
if (_tables.empty()) return end();
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size(); // 计算桶位置
Node* cur = _tables[hashi];
while (cur) {
// 遍历单链表,对比 Key
if (kot(cur->_data) == key) {
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
查找过程:
查找过程:计算哈希值 -> 定位桶索引 -> 遍历该桶的单链表。 时间复杂度:平均情况 O(1),极端冲突退化为单链表时为 O(N)。
bool Erase(const K& key) {
if (_tables.empty()) return false;
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur) {
if (kot(cur->_data) == key) {
// 情况 A:要删除的是桶的第一个节点(头节点)
if (prev == nullptr) {
_tables[hashi] = cur->_next;
}
// 情况 B:要删除的是中间节点
else {
prev->_next = cur->_next;
}
delete cur; // 释放内存
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false; // 没找到
}
删除要点:
必须维护前驱指针
prev来进行链表的缝合。 必须区分头节点删除和中间节点删除。 如果是头节点,需要直接修改数组中保存的指针_tables[hashi]。
通过这份代码,我们完整地复现了 C++ STL 哈希表底层最核心的三大机制:
开散列(哈希桶)解决冲突:利用数组 + 单链表,完美避免了闭散列的踩踏效应。
KeyOfT泛型萃取:通过传入提取器仿函数,打破了set和map的隔阂,实现了底层结构的完美复用。 零拷贝素数扩容:通过预打表的素数数组控制容量,通过节点直接迁移实现极速扩容。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online