跳到主要内容C++算法
哈希表进阶:用哈希桶封装 unordered_set 和 unordered_map 及迭代器实现
哈希表进阶实现基于哈希桶结构封装 C++ 标准库的 unordered_set 和 unordered_map。重点阐述模板参数设计、仿函数获取键值、单向迭代器的重载与实现逻辑,以及扩容机制。通过对比 set 与 unordered_set 性能,展示哈希表优势,并提供完整代码示例。
二进制5 浏览 一、常见接口详解(C++标准库)
文档:《unordered_set》,《unordered_map》
这个我们已经是老生常谈了。
1.1、unordered_set / unordered_map
unordered_set 和 unordered_map 的接口几乎完全相同,所以我们以 unordered_set 为例讲解:
| 构造相关接口(常用) |
unordered_set ( const unordered_set& ust ); | 拷贝构造 |
unordered_set ( InputIterator first, InputIterator last ) | 迭代器区间构造 |
unordered_set ( initializer_list<value_type> il) | 初始化列表构造 |
| 其他接口(常用) |
size_type size() | 计算哈希表中的数据个数 |
size_type count ( const key_type& k ) | 统计值为 k 的数据个数 |
pair<iterator,bool> insert ( const value_type& val ) | 插入(注意返回值类型) |
iterator erase ( const_iterator position ) | 删除 |
| 迭代器 |
iterator begin() const_iterator begin() const | 返回哈希表第一个存储有效数据位置的迭代器。 |
iterator end() const_iterator end() const | 返回哈希表最后一个位置之后位置的迭代器 |
1.2、接口测试
unordered---无序,由于数据是根据映射关系映射到哈希表中的,所以说哈希表中的数据是无序的。同时,unordered_set 和 unordered_map不支持数据冗余。数据需要重复,就要用:unordered_multiset 和 unordered_multimap
💦插入 / 遍历比较:
💦性能测试
void test02() { size_t N = 10000000; unordered_set<int> us; set<int> st; vector<int> v; v.reserve(N); srand(time(0)); for (size_t i = 0; i < N; i++) { v.push_back(rand() + i); }
二、哈希桶实现(进阶)
对哈希桶的实现,我们复用前面的代码逻辑,同时再添加一个迭代器,就大功告成了。
2.1、模板参数说明
由于我们要用哈希表封装 unordered_set 和 unordered_map。所以,对于数据类型就不能够写死。即我们实现的模板既要适配 unordered_set 的 key 值类型,还要适配 unordered_map 的 key_value(键值对)的类型。
解决方案:用一个 T 来指代哈希表的数据类型,你传什么类型我就是什么类型。
猜你又要说:你用 T 指代数据类型,那我的 key(键值) 怎么搞?
好办,不是有仿函数嘛。我给你的模板参数加一个仿函数。仿函数:'我在 unordered_set 和 unordered_map 中给你把 key 传过来'。
但你是对的!哈希表最重要的就是:把数据映射到对应的位置,然后才能挂数据(哈希桶)。你要知道,哈希表底层核心载体是数组。你要挂数据,得找到数组的下标不是。我们主要用取余数的方法来找到 key 应该映射到数组的哪个位置,即 key 对应的下标。
【高阶数据结构】哈希表 一文中已经做了详细地介绍。那好,5 % 11 = 5,那 key = -9,key = "string"呢,我可没说我的 key 是什么类型。
好办,不是有仿函数嘛。我给你的模板参数再加一个仿函数。仿函数:'我把所有的key 都给你转成无符号整形(unsigned int)'。
到这里,哈希表的所有模板参数就到欧克了。代码框架如下:
template<class K, class T, class keyOfT, class Hash> class Hash_Bucket {
typedef HashNode<T> Node;
public:
Hash_Bucket() :_tables(__stl_next_prime(0)) , _size(0) {}
private:
vector<Node*> _tables;
size_t _size = 0;
};
2.2、获取键值---仿函数
由于我们在查找,比较过程中都需要用到 key,而哈希表的模板中 T 为一个泛型。所以,我们在 unordered_set 和 unordered_map 各自的类中实现一个获取 key 的仿函数。方便模板类中使用 key。
我们知道:仿函数其实就是一个类,重载了 operator()(给不知道的小伙伴补充一下)。
对于 unordered_set,key 就是其存储的数据,直接返回即可。
对于 unordered_map,存储的是一个 pair 类型的对象,key 就是 pair 对象的第一个值,返回 first 即可。
struct keyOfSet {
const K& operator()(const K& key) { return key; }
};
struct keyOfMap {
const K& operator()(const pair<K, V>& kv)
{ return kv.first; }
};
2.3、迭代器
不简单,确实不简单。模板参数跟 Hash_Bucket 差不多。需要注意:迭代器有普通迭代器和 const 迭代器,所以,也不能写死。这就需要再增加几个模板参数,保证模板是泛型的。
抽象,属实抽象,增加啥呢?你得想想,普通迭代器和 const 迭代器有什么不同呢:那不就是数据类型嘛,毕竟迭代器类型就是根据数据类型决定的。
再提示一下:迭代器里面需要实现重载(解引用操作符),重载->,重载 == 和 != ,以及重载++**。== 和 != 返回值为 bool;++返回值为迭代器;就剩下 * 和 -> 了。也就是说:(解引用)的返回值为 T&还是 const T&;->的返回值是 T* 还是 const T*。
好办,再增加几个模板参数嘛!用 Ref 指代 T&和 const T& ,Ptr 指代 T* 和 const T*。用两个泛型类型。
既然迭代器是一个类模板,那他有什么成员变量吗?这也是我们理解迭代器的关键!!!实际上迭代器底层就是一个指向当前节点的指针。有了这个节点的指针,我们才能实现:**
(解引用操作符),>, == 和 != ,++这些操作。*
剧透一下:重载 operator++时由于要取模,就要获取哈希表的大小,即_tables.size()。
所以,我们还需要一个指向哈希表的指针,通过这个指针我们才能获取哈希表的大小。
对于第二个成员变量(指向哈希表的指针)。需要我们结合使用场景来确定。但第一个成员变量几乎就是众多迭代器的不可或缺的一员。
template<class K, class T, class Ref, class Ptr, class keyOfT, class Hash> struct HTIterator {
typedef HashNode<T> Node;
typedef Hash_Bucket<K, T, keyOfT, Hash> Hash_Bucket;
typedef HTIterator<K, T, Ref, Ptr, keyOfT, Hash> Self;
Node* _node;
const Hash_Bucket* _ht;
HTIterator(Node* node,const Hash_Bucket* ht) :_node(node) ,_ht(ht) { }
};
2.3.1、重载 operator++
你肯定会好奇:为什么只有++呢?这是因为,这里的迭代器是单向迭代器。
为什么是单向的?因为哈希表上挂的每个桶不都是单链表嘛,可不就是单向的。
那怎么找到当前节点下一个位置的迭代器?需要我们分情况讨论,相信大家已经见怪不怪了。
1)当迭代器在桶的中间部分时:让当前节点的指针更新到下一个节点即可。
2)当前节点已经是桶的最后一个节点或当前节点就是该位置的唯一一个节点:这时候,就需要我们向哈希表的后面找第一个不为空的位置,然后指向节点的指针更新。
你说巧不巧:哈希表后面所有的位置都是空的。势必就会找到结尾,此时就需要我们让指向节点的指针指向 nullptr,然后返回。
Self& operator++() {
if (_node->_next) {
_node = _node->_next;
}
else {
keyOfT kot;
Hash hash;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size()) {
_node = _ht->_tables[hashi];
if (_node) break;
else hashi++;
}
if (hashi == _ht->_tables.size()) {
_node = nullptr;
}
return *this;
}
}
极其极其细心的小伙伴又会发现:_tables 不是 Hash_Bucket 的私有成员变量吗?你怎么还光明正大的用上了。
必须表扬,这位同学非常细心,能成大事。那怎么办呢?前面不是学过友元函数嘛。我们可以试着将迭代器的类声明为 Hash_Bucket 的友元类。你还别说,真就成了。
template<class K, class T, class Ref, class Ptr, class keyOfT, class Hash> friend struct HTIterator;
2.3.2、重载其他
Ref operator*() { return _node->_data; }
Ptr operator->() { return &_node->_data; }
bool operator==(const Self& s) const { return _node == s._node; }
bool operator!=(const Self& s) const { return _node != s._node; }
三、封装
接下来我们直接封装,这就变得非常 easy 了。只需要用一个哈希表的对象调用我们在哈希表中实现的接口就好了。
3.1、unordered_set 封装
#include"Hash_Bucket.h"
template<class K,class Hash=HashFun<K>> class UnorderedSet {
struct keyOfSet {
const K& operator()(const K& key) { return key; }
};
public:
typedef typename Hash_Bucket<K, const K, keyOfSet, Hash>::Iterator iterator;
typedef typename Hash_Bucket<K, const K, keyOfSet, Hash>::ConstIterator const_iterator;
iterator begin() { return _ht.Begin(); }
iterator end() { return _ht.End(); }
const_iterator begin() const { return _ht.Begin(); }
const_iterator end() const { return _ht.End(); }
pair<iterator, bool> insert(const K& key)
{ return _ht.Insert(key); }
iterator find(const K& key)
{ return _ht.Find(key); }
bool erase(const K& key)
{ return _ht.Erase(key); }
private:
Hash_Bucket<K, const K, keyOfSet, Hash> _ht;
};
3.2、unordered_map 封装
重载 operator[]我们再啰嗦几句:只有 unordered_map / map 重载 operator[]; [] 需要支持插入 + 查找 + 修改,的功能。
#include"Hash_Bucket.h"
template<class K,class V,class Hash=HashFun<K>> class UnorderedMap {
struct keyOfMap {
const K& operator()(const pair<K, V>& kv)
{ return kv.first; }
};
public:
typedef typename Hash_Bucket<K, pair<const K, V>, keyOfMap, Hash>::Iterator iterator;
typedef typename Hash_Bucket<K, pair<const K, V>, keyOfMap, Hash>::ConstIterator const_iterator;
iterator begin() { return _ht.Begin(); }
iterator end() { return _ht.End(); }
const_iterator begin()const { return _ht.Begin(); }
const_iterator end()const { return _ht.End(); }
V& operator[](const K& key)
{ pair<iterator, bool> ret = insert({ key,V() }); return ret.first->second; }
pair<iterator,bool> insert(const pair<K, V>& kv)
{ return _ht.Insert(kv); }
iterator find(const K& key)
{ return _ht.Find(key); }
bool erase(const K& key)
{ return _ht.Erase(key); }
private:
Hash_Bucket<K, pair<const K, V>, keyOfMap, Hash> _ht;
};
四、拓展接口说明(C++标准库)
unordered_set 和 unordered_map 对于这些接口功能也相同,下面以 unordered_map 为例讲解。
4.1、哈希表大小——bucket_count
void test01() {
unordered_map<string,string> str;
vector<string> v({ "sort","left","insert","hdszm","a","b","c","d","e"});
size_t count = str.bucket_count();
cout << "bucket_count: " << count << endl;
for (auto e : v) {
str.insert({ e,e });
}
count = str.bucket_count();
cout << "bucket_count: " << count << endl;
}
不难看出:bucket_count 函数就是来获取当前哈希表的大小的。初始大小为 8,当我们插入 9 个数据后,扩容为 64。
4.2、对应位置处的数据个数——bucket_size
void test02() {
unordered_map<string, string> str;
vector<string> v({ "sort","left","insert","hdszm","a","b","c","d"});
size_t size = str.bucket_size(2);
cout << "bucket_size: " << size << endl;
for (auto e : v) {
str.insert({ e,e });
}
size = str.bucket_size(2);
cout << "bucket_count: " << size << endl;
}
4.3、找数据所在位置——bucket
void test03() {
unordered_map<string, string> mymap = { {"us","United States"},{"uk","United Kingdom"}, {"fr","France"},{"de","Germany"} };
for (auto& x : mymap) {
cout << "Element [" << x.first << ":" << x.second << "]";
cout << " is in bucket #" << mymap.bucket(x.first) << std::endl;
}
cout << "us: " << ('u' + 's') % 8 << endl;
cout << "uk: " << ('u' + 'k') % 8 << endl;
cout << "fr: " << ('f' + 'r') % 8 << endl;
cout << "de: " << ('d' + 'e') % 8 << endl;
}
返回 key 在哈希表中的相对位置,即数组的下标 +1。
4.4、负载因子——load_factor
void test04() {
unordered_map<string, string> mymap = { {"us","United States"},{"uk","United Kingdom"}, {"fr","France"},{"de","Germany"}};
cout << "load_factor = " << mymap.load_factor() << std::endl;
}
4.5、扩容——rehash/reserve
void test05() {
unordered_map<std::string, std::string> mymap1;
unordered_map<std::string, std::string> mymap2;
mymap1.rehash(20);
mymap2.reserve(20);
cout << "mymap1_rehash: " << mymap1.bucket_count() << endl;
cout << "mymap2_reserve: " << mymap2.bucket_count() << endl;
}
五、完整代码
<<
相关免费在线工具
- 加密/解密文本
使用加密算法(如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