跳到主要内容C++ STL 哈希表原理与模拟实现 | 极客日志C++算法
C++ STL 哈希表原理与模拟实现
综述由AI生成哈希表的基本概念、负载因子、哈希冲突及解决方法(线性探测、哈希桶)。详细讲解了哈希函数的设计原则,并分别使用线性探测法和哈希桶法模拟实现了哈希表类。最后基于哈希桶结构,引入迭代器,模拟实现了 C++ STL 中的 unordered_map 和 unordered_set,包含完整的代码示例与测试验证。
云间运维31 浏览 一、哈希表介绍
1.1 基本介绍
什么是哈希表?
哈希表是一种通过键(Key)来直接访问值(Value)的数据结构,它利用一个叫做哈希函数的公式,把任意大小的键转换成一个固定大小的数字,这个数字被称为哈希值或哈希码。然后,用这个哈希值作为数组的索引,将值存储在这个索引对应的数组位置上。
哈希表的本质是一个数组
哈希表本质上是一个基于数组实现的数据结构,其核心设计思想是利用数组的随机访问特性,实现快速的插入、删除和查找操作。
具体来说,它通过一个哈希函数将键映射为一个整数值,这个值经过处理(通常是对数组长度取模)后,作为该键值对在数组中的存储下标。
理想情况下,每个键都能通过哈希函数得到一个唯一的、不冲突的下标,这样我们就可以直接将值存放到数组的对应位置,访问时也能立刻定位——时间复杂度接近 O(1)。
然而,现实应用中,哈希函数很难做到为所有可能的键生成独一无二的索引。因为键的空间通常远大于数组的容量,所以不同的键有可能计算出相同的数组下标,这就是所谓的哈希冲突。
常见的冲突解决方法有两种:
- 链地址法(Separate Chaining):数组的每个位置存放一个链表的头指针。当多个键映射到同一个下标时,它们会依次被挂在这个位置的链表上。Java 的
HashMap 就采用这种思路。
- 开放地址法(Open Addressing):当发生冲突时,算法会按照某种探测规则在数组内寻找下一个空闲的位置,并将值存入该处。
为了保持高效,哈希表还需要动态调整大小。通常会设定一个负载因子阈值,当元素数量与数组长度的比值超过该阈值时,就对数组进行扩容,将容量扩大,并将所有现有元素重新哈希到新数组中。
核心组件
一个哈希表主要由以下三个核心部分组成:
- 键(Key):标识符。
- 值(Value):实际数据。
- 哈希函数(Hash Function):将键映射到数组索引的数学函数。
1.2 负载因子
哈希表的效率高度依赖于一个关键指标:负载因子。
负载因子 = 哈希表中已存储的元素数量 / 哈希表数组的总大小
- 负载因子低:意味着数组还有很多空位,冲突概率小,操作速度快。
- 负载因子高:意味着数组快满了,冲突概率急剧增加,性能下降。
为了保持高性能,当负载因子超过某个阈值(例如 0.75)时,哈希表会进行扩容操作:创建一个新的更大的数组,遍历旧哈希表中的所有键值对,根据新的数组大小重新计算索引并插入。
这个过程称为 Rehashing,虽然耗时,但能显著降低负载因子,使哈希表恢复高效。
1.3 哈希冲突
哈希冲突,简单来说,就是两个不同的输入值,经过同一个哈希函数计算后,得到了相同的输出值。
从数学上讲,哈希函数是一个压缩映射,它的输出空间通常远小于输入空间。根据鸽巢原理,当输入数量超过输出数量时,必然存在至少一个输出值对应多个输入值。
常见的解决哈希冲突的方法
- 开放寻址法:如果当前计算出的位置已被占用,就按照某种规则在哈希表中寻找下一个空闲位置。
- 线性探测:从冲突位置开始,依次向后检查下一个位置。
- 二次探测:探测步长变为平方序列。
双重散列:使用两个哈希函数计算步长。链地址法(拉链法):哈希表的每个槽位存放一个链表。冲突发生时,新元素直接插入对应槽位的链表中。再哈希法:准备一组哈希函数,当第一个发生冲突时,换另一个计算新地址。建立公共溢出区:将哈希表分为基本表和溢出表。
哈希冲突的影响
- 性能下降:冲突都会增加一次操作的时间。理想情况下哈希表操作是 O(1),冲突后可能退化为 O(n)。
- 设计权衡:好的哈希函数应尽量让元素分布均匀,减少冲突概率。
1.4 哈希函数
哈希函数负责将关键码(Key)映射为一个数组下标(哈希地址)。
- 定义域与值域的匹配:值域必须正好是数组索引区间。
- 均匀分布:计算出的地址应该尽可能均匀地分布在整个空间中。
- 简单高效:计算应该足够简单、快速。
两种常见的哈希函数构造方法
公式:Hash(Key) = A * Key + B
直接使用关键码本身(或它的线性变换)作为哈希地址。适用于关键码是整数且取值范围较小且连续的情况。
其中 p 是一个不大于哈希表长度 m 的质数。这种方法能将任意整数关键码压缩到 0 ~ p-1 的范围内。
1.5 解决哈希冲突的常用方法
我们来深入探讨哈希冲突解决的两种常用方法:线性探测法和哈希桶(即链地址法)。
1.5.1 线性探测法
- 原理:属于开放寻址法的一种。当发生哈希冲突时,它会依次检查冲突位置的下一个位置,直到找到一个空闲的槽位。
- 操作过程:
- 插入:计算哈希地址,若被占用则向后探测,直到找到空位。
- 查找:计算哈希地址,若不相等则继续线性探测,直到遇到空槽或遍历一圈。
- 删除:不能简单地将槽位置空,通常采用惰性删除:将删除的槽位标记为'已删除',查找时遇到'已删除'标记继续探测。
- 优点:实现简单,无需额外指针,缓存友好。
- 缺点:容易产生聚集现象,删除麻烦,对负载因子敏感。
1.5.2 哈希桶
- 原理:将哈希表的每个槽位设置为一个链表的头节点。所有哈希地址相同的元素都放入对应槽位的链表中。
- 操作过程:
- 插入:找到对应桶的链表,将新节点插入链表。
- 查找:计算索引,然后在对应链表中顺序查找。
- 删除:在链表中找到目标节点并删除。
- 优点:简单直接,删除方便,哈希表永远不会填满。
- 缺点:需要额外的指针空间,缓存不友好。
二、使用线性探测法模拟实现哈希表
enum STATE {
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData {
pair<K, V> _kv;
STATE _state = EMPTY;
};
2.1 哈希函数
我们需要定义哈希函数对象。对于不同类型(如整数、字符串),可以使用模板特化。
template<class K>
struct DefaultHashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
template<>
struct DefaultHashFunc<string> {
size_t operator()(const string& str) {
size_t hash = 0;
for (auto ch : str) {
hash *= 131;
hash += ch;
}
return hash;
}
};
2.2 哈希表类
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable {
public:
HashTable() {
_table.resize(10);
}
private:
vector<HashData<K, V>> _table;
size_t _n = 0;
};
2.2.1 插入操作
整个插入操作分为两步:判断是否需要扩容,以及通过线性探测法插入新元素。
bool Insert(const pair<K, V>& kv) {
if (_n * 10 / _table.size() >= 7) {
size_t newSize = _table.size() * 2;
HashTable<K, V, HashFunc> newHT;
newHT._table.resize(newSize);
for (size_t i = 0; i < _table.size(); i++) {
if (_table[i]._state == EXIST) {
newHT.Insert(_table[i]._kv);
}
}
_table.swap(newHT._table);
}
HashFunc hf;
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;
}
2.2.2 查询操作
HashData<const K, V>* Find(const K& key) {
HashFunc hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._state != EMPTY) {
if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key) {
return (HashData<const K, V>*)&_table[hashi];
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
2.2.3 删除操作
bool Erase(const K& key) {
HashData<const K, V>* ret = Find(key);
if (ret) {
ret->_state = DELETE;
--_n;
return true;
}
return false;
}
2.3 完整代码 + 测试
(此处省略完整的头文件 test.cpp 代码展示,主要包含插入、查找、删除及扩容测试逻辑)
三、使用哈希桶模拟实现哈希表
哈希桶(Hash Bucket)就是链地址法的一种实现方式。每个桶对应一个链表,所有映射到同一索引的键值对都存储在这个桶所指向的链表中。
3.1 哈希函数
3.2 哈希表类
template <class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
public:
HashTable() {
_table.resize(10, nullptr);
}
~HashTable() {
for (size_t i = 0; i < _table.size(); i++) {
Node *cur = _table[i];
while (cur) {
Node *next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
private:
vector<Node *> _table;
size_t _n = 0;
};
3.2.1 插入操作
bool Insert(const pair<K, V> &kv) {
if (Find(kv.first)) {
return false;
}
HashFunc hf;
if (_n == _table.size()) {
size_t newSize = _table.size() * 2;
vector<Node *> newTable;
newTable.resize(newSize, nullptr);
for (size_t i = 0; i < _table.size(); i++) {
Node *cur = _table[i];
while (cur) {
Node *next = cur->_next;
size_t hashi = hf(cur->_kv.first) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t hashi = hf(kv.first) % _table.size();
Node *newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
3.2.2 查询操作
Node *Find(const K &key) {
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node *cur = _table[hashi];
while (cur) {
if (cur->_kv.first == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
3.2.3 删除操作
bool Erase(const K &key) {
HashFunc hf;
size_t hashi = hf(key) % _table.size();
Node *prev = nullptr;
Node *cur = _table[hashi];
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) {
_table[hashi] = cur->_next;
} else {
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
3.3 完整代码 + 测试
(包含完整的 HashTable.hpp 和 test.cpp 实现,支持整型和字符串类型测试)
四、模拟实现 unordered_map 和 unordered_set
C++ STL 库里面的 unordered_map 和 unordered_set 的底层实现都是基于哈希表来模拟实现的。
4.1 引入迭代器
这段代码实现了一个哈希表的正向迭代器,支持普通迭代器和 const 迭代器。
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
struct HTIterator {
typedef HashNode<T> Node;
typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;
Node* _node;
const HashTable<K, T, KeyOfT, HashFunc>* _pht;
};
4.2 修改哈希表代码
引入迭代器后,需要修改哈希表类以支持 begin() 和 end() 接口,并修改 Insert 返回值为 pair<iterator, bool>。
4.3 模拟实现 unordered_map
namespace mySTL {
template<class K, class V>
class unordered_map {
struct MapKeyOfT {
const K& operator()(const pair<const K, V>& kv) {
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
pair<iterator, bool> insert(const pair<K, V>& kv) {
return _ht.Insert(kv);
}
V& operator[](const K& key) {
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;
};
}
4.4 模拟实现 unordered_set
namespace mySTL {
template<class K>
class unordered_set {
struct SetKeyOfT {
const K& operator()(const K& key) {
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
pair<iterator, bool> insert(const K& key) {
pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
return pair<iterator, bool>(ret.first, ret.second);
}
private:
hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
};
}
(测试程序验证了插入、查找、删除及迭代器遍历功能)
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online