【哈希表封装实现】—— 我与C++的不解之缘(二十九)

【哈希表封装实现】—— 我与C++的不解之缘(二十九)

前言

我们知道unordered_setunordered_map的底层是哈希表,那现在我们就是使用我们自己实现的HashTable来封装实现出简单的unordered_setunordered_map

一、了解源码

SGL-STL30版本之前源代码中是没有unordered_setunordered_map

unordered_setunordered_mapC++11之后才更新的;SGL-STL30C++11之前的版本

但是想SGL-STL30中实现了哈希表,但容器的名字叫做hash_maphash_set(它是作为非标准的容器出现的)

源代码在hash_map/hash_set/stl_hash_map/stl_hash_map/stl_hash_set/stl_hashtable.h

这里截取其中的一部分来看一下:

// stl_hash_settemplate<classValue,classHashFcn= hash<Value>,classEqualKey= equal_to<Value>,classAlloc= alloc>classhash_set{private:typedef hashtable<Value, Value, HashFcn, identity<Value>, EqualKey, Alloc> ht; ht rep;public:typedeftypenameht::key_type key_type;typedeftypenameht::value_type value_type;typedeftypenameht::hasher hasher;typedeftypenameht::key_equal key_equal;typedeftypenameht::const_iterator iterator;typedeftypenameht::const_iterator const_iterator; hasher hash_funct()const{return rep.hash_funct();} key_equal key_eq()const{return rep.key_eq();}};// stl_hash_maptemplate<classKey,classT,classHashFcn= hash<Key>,classEqualKey= equal_to<Key>,classAlloc= alloc>classhash_map{private:typedef hashtable<pair<const Key, T>, Key, HashFcn, select1st<pair<const Key, T>>, EqualKey, Alloc> ht; ht rep;public:typedeftypenameht::key_type key_type;typedef T data_type;typedef T mapped_type;typedeftypenameht::value_type value_type;typedeftypenameht::hasher hasher;typedeftypenameht::key_equal key_equal;typedeftypenameht::iterator iterator;typedeftypenameht::const_iterator const_iterator;};// stl_hashtable.htemplate<classValue,classKey,classHashFcn,classExtractKey,classEqualKey,classAlloc>classhashtable{public:typedef Key key_type;typedef Value value_type;typedef HashFcn hasher;typedef EqualKey key_equal;private: hasher hash; key_equal equals; ExtractKey get_key;typedef __hashtable_node<Value> node; vector<node*, Alloc> buckets; size_type num_elements;public:typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> iterator; pair<iterator,bool>insert_unique(const value_type& obj); const_iterator find(const key_type& key)const;};template<classValue>struct__hashtable_node{ __hashtable_node* next; Value val;};
简单了解一下源码:(这里和map/set大致是一样的)

这里hashtable实现的是<V,K>结构,第一个参数V表示存储的数据类型,第二个参数K表示关键值key

结构上依然是unordered_setunordered_map使用同一个哈希表实现KeyKey/Value结构;unordered_set<Key,Key>unordered_map<pair<Key,Value>,Key>

这里呢它第一个参数是Value,感觉有点怪怪的;我们在实现的时候依然按照<Key,Value>来实现。

二、封装实现unordered_setunordered_map

1. 复用哈希表框架,实现insert

  • 根据我们上篇博客实现的哈希表,我们要复用这个框架,写出来unordered_setunordered_map的大致框架并支持insert操作。
  • 这里我们就按照红黑树封装实现setmap的逻辑,Key参数就用KValue参数就用V来实现;对于哈希表存储的数据类型,就用T来表示。
  • 这里我们将数据类型用T来表示,就导致了我们HashTable中不知道TK还是pair<K,V>,所以我们要像实现setmap那样,用一个模版参数KeyOfT来让我们在HashTable中能通过T取到K
  • 那这个KeyOfT就只能从unordered_setunordered_map中传(因为在unordered_setunordered_map中才知道数据类型是K还是pair<K,V>
这里再重述一下为什么要用KeyOfT因为我们不知道T到底是K还是pair<K,V>

如果是K那可以直接进行转换成整型然后取模操作。

但如果是pair<K,V>pair<K,V>是不支持转换成整型的,所以我们要实现KeyOfT通过T取出K,然后对K进行转化整型然后取模操作。

逻辑理清楚了,现在来代码(这里有了KeyOfT,那每次使用Key转整型并取模操作之前都要套上一层取Key操作)。

先来看我们的HashTable.h

template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& str){ size_t ret =1;int i =1;for(auto& ch : str){ ret += ch * i; i++;}return ret;}};template<classT>structHashNode{HashNode(const T& data):_data(data),_next(nullptr){} T _data; HashNode<T>* _next;};template<classK,classT,classKeyOfT,classHash= HashFunc<K>>classHashTable{typedef HashNode<K, T> Node;public:HashTable(){ _table.resize(__stl_next_prime(0));}boolinsert(const T& data){ KeyOfT kof;if(find(kof(data))!=nullptr)returnfalse; Hash hs;//扩容if((double)_n /(double)_table.size()>=1){ size_t newsize =__stl_next_prime(_table.size()+1); vector<Node*> newtable; newtable.resize(newsize);for(int i =0; i < _table.size(); i++){ Node* cur = _table[i];while(cur){ Node* next = cur->_next; size_t hash0 =hs(kof(cur->_data))% newsize;//size_t hash0 = hs(cur->_kv.first) % newsize;//size_t hash0 = cur->_kv.first % newsize; cur->_next = newtable[hash0]; newtable[hash0]= cur; cur = next;}} _table.swap(newtable);}//插入 size_t hash0 =hs(kof(data))% _table.size();//size_t hash0 = hs(kv.first) % _table.size();//size_t hash0 = kv.first % _table.size();//头插到当前位置 Node* newnode =newNode(data); newnode->_next = _table[hash0]; _table[hash0]= newnode; _n++;returntrue;} Node*find(const K& key){ Hash hs; KeyOfT kof; size_t hashi =hs(key)% _table.size();//size_t hashi = key % _table.size(); Node* cur = _table[hashi];while(cur){if(kof(cur->_data)== key)return cur; cur = cur->_next;}returnnullptr;}boolerase(const K& key){ KeyOfT kof;for(int i =0; i < _table.size(); i++){ Node* cur = _table[i]; Node* prve =nullptr;while(cur){if(key ==kof(cur->_data))//if (key == cur->_kv.first){if(prve ==nullptr){ _table[i]= cur->_next;}else{ prve->_next = cur->_next;}delete cur;--_n;returntrue;} prve = cur; cur = cur->_next;}}}~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;}}private: vector<Node*> _table; size_t _n =0;};

再来看unordered_set.h

namespace HL {template<classK>classunordered_set{structSetKeyOfT{const K&operator()(const K& key){return key;}};public:boolinsert(const K& key){return _hash.insert(key);}private: HashTable<K,const K, SetKeyOfT> _hash;};voidtest_set(){ unordered_set<int> ust;for(int i =0; i <=54; i++){int x =rand(); ust.insert(x);}}}

最后再来看unordered_map.h

namespace HL {template<classK,classV>classunordered_map{structMapKeyOfT{const K&operator()(const pair<K,V>& kv){return kv.first;}};public:boolinsert(const pair<K, V>& kv){return _hash.insert(kv);}private: HashTable<K, pair<const K, V>, MapKeyOfT> _hash;};voidtest_map(){ unordered_map<int,int> ump;for(int i =0; i <=54; i++){int x =rand(); ump.insert({ x,i });}}}
这里对于初步实现的unordered_setunordered_map,博主还写了两个测试方法test_unordered_settest_unordered_map

在测试方法中插入了55个数据,也涉及到了扩容(且没有出事初始化随机数生成器,每次生成的随机数都是一样的;方便测试)。

2. 实现iterator迭代器,完成unordered_setunordered_map的迭代器遍历

HashTable的迭代器:

对于我们这里实现的链式结构的HashTable,它的迭代器显而易见是单向的;

unordered_setunordered_map的迭代器也是单向的。

这里我们HashTable的迭代器,和list大致是一样的,都是封装节点的指针,再通过运算符重载实现;让迭代器像指针一种访问。

**对于HashTable**的迭代器中==!=*->,实现这些运算符重载都好说;那operator++呢?

operator++如何实现呢?

我们迭代器是对节点指针的封装,但是我们节点HashNode中只存放了数据_data和下一个位置的指针_next。如果我们当前节点下一个位置不为空(当前桶(位置)还有节点,那就直接指向下一个节点即可。那如果我们当前位置下一个位置为空(当前桶遍历完了),我们就要想办法找大下一个不为空的桶。

那如何去找下一个桶呢?

如果我们单纯的对节点的指针进行封装,显然是不能解决这个问题的;参照源码我们可以发现,iterator中除了节点的指针,还存在一个哈希表对象的指针。

有了哈希表对象的指针,我们走完当前桶那就很容易找到下一个桶了。

这里我们找到下一个桶位置需要用到KeyOfT进行取Key操作,也要用到HashKey取模操作

所以我们iterator的模版参数就不只是原来的template<class T,classRef,class Ptr>了。

而是template<class K,class T,classRef,class Ptr,class KeyOfT,class Hash>

那现在问题又出现了:

我们HashIterator是定义在HashTable前面的,而我们HashIterator中还要使用HashTable,并且在HashIterator中还用访问HashTable的私有成员,那怎么办呢?首先我们要对HashTable进行前置声明,让我们在HashIterator中知道有HashTable这个类。然后就是友元,让HashIterator成为HashTable类的友元。

const迭代器

对于const迭代器实现起来就好很多了,只需要在HashIterator第二和第三个参数位置传const T&,const T*即可。

那这样我们大体就清楚了,现在来实现代码

//前置声明template<classK,classT,classKeyOfT,classHash>classHashTable;template<classK,classT,classRef,classPtr,classKeyOfT,classHash>classHashIterator{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;public:booloperator==(const Self& it){return _node == it._node;}booloperator!=(const Self& it){return _node != it._node;} Ref operator*(){return _node->_data;} Ptr operator->(){return&_node->_data;} Self&operator++(){if(_node->_next !=nullptr){ _node = _node->_next;}else{//当前桶走完了,找到下一个桶 KeyOfT kof; Hash hs; size_t hashi =hs(kof(_node->_data))% _ht->_table.size(); hashi++;while(hashi < _ht->_table.size()){if(_ht->_table[hashi]){ _node = _ht->_table[hashi];break;} hashi++;}if(hashi == _ht->_table.size()) _node =nullptr;}return*this;}HashIterator(Node* node,const HT* ht):_node(node),_ht(ht){}private: Node* _node;const HT* _ht;};

当然不要忘了在HashTable中声明友元

这里就只展示出了HashTable中新增的部分。
template<classK,classT,classKeyOfT,classHash= HashFunc<K>>classHashTable{//友元template<classK,classT,classRef,classPtr,classKeyOfT,classHash>friendclassHashIterator;public:typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HashIterator<K, T,const T&,const T*, KeyOfT, Hash> ConstIterator;//迭代器部分 Iterator begin(){//找到第一个迭代器的位置for(int i =0; i < _table.size(); i++){if(_table[i]!=nullptr)return{ _table[i],this};}return{nullptr,this};} Iterator end(){return{nullptr,this};} ConstIterator begin()const{//找到第一个迭代器的位置for(int i =0; i < _table.size(); i++){if(_table[i]!=nullptr)return{ _table[i],this};}return{nullptr,this};} ConstIterator end()const{return{nullptr,this};}};

unordered_setunordered_map迭代器:

对于unordered_setunordered_map迭代器对HashTable的迭代器进行复用即可。

unordered_set迭代器

typedeftypenameHashTable<K,const K, SetKeyOfT>::Iterator iterator;typedeftypenameHashTable<K,const K, SetKeyOfT>::ConstIterator const_iterator; iterator begin(){return _hash.begin();} iterator end(){return _hash.end();} const_iterator begin()const{return _hash.begin();} const_iterator end()const{return _hash.end();}

unordered_map迭代器

typedeftypenameHashTable<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;typedeftypenameHashTable<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator; iterator begin(){return _hash.begin();} iterator end(){return _hash.end();} const_iterator begin()const{return _hash.begin();} const_iterator end()const{return _hash.end();}
这里博主还写出了测试迭代器的代码(只有普通迭代器的
voidtest_set(){ unordered_set<int> ust;for(int i =0; i <=54; i++){int x =rand(); ust.insert(x);}auto it = ust.begin();while(it != ust.end()){ cout <<*it << endl;++it;} cout << endl;}voidtest_map(){ unordered_map<int,int> ump;for(int i =0; i <=54; i++){int x =rand(); ump.insert({ x,i });}auto it = ump.begin();while(it != ump.end()){ cout << it->first <<":"<< it->second << endl;++it;} cout << endl;}

3. 实现unordered_mapoperator[]

在这里插入图片描述

对于unordered_mapoperator[],其功能和mapoperator[]一样,如果key值存在就返回key对应的value的引用;如果key不存在就先插入keyvalue是缺省值),再返回value的引用。

那我们要实现operator[]就要依赖于insert,所以我们要对insert进行修改(将其返回值修改为pair<Iterator,bool>

这里我们insert又依赖于find判断数据是否存在,所以对find也要进行一点点修改。

HashTableinsertfind

//bool insert(const T& data) pair<Iterator,bool>insert(const T& data){ KeyOfT kof;//if (find(kof(data)) != nullptr)// return false; Iterator it =find(kof(data));if(it !=end())return{ it,false}; Hash hs;//扩容if((double)_n /(double)_table.size()>=1){ size_t newsize =__stl_next_prime(_table.size()+1); vector<Node*> newtable; newtable.resize(newsize);for(int i =0; i < _table.size(); i++){ Node* cur = _table[i];while(cur){ Node* next = cur->_next; size_t hash0 =hs(kof(cur->_data))% newsize;//size_t hash0 = hs(cur->_kv.first) % newsize;//size_t hash0 = cur->_kv.first % newsize; cur->_next = newtable[hash0]; newtable[hash0]= cur; cur = next;}} _table.swap(newtable);}//插入 size_t hash0 =hs(kof(data))% _table.size();//size_t hash0 = hs(kv.first) % _table.size();//size_t hash0 = kv.first % _table.size();//头插到当前位置 Node* newnode =newNode(data); newnode->_next = _table[hash0]; _table[hash0]= newnode; _n++;//return true;return{{newnode,this},true};}//Node* find(const K& key) Iterator find(const K& key){ Hash hs; KeyOfT kof; size_t hashi =hs(key)% _table.size();//size_t hashi = key % _table.size(); Node* cur = _table[hashi];while(cur){if(kof(cur->_data)== key)return{ cur,this};//return cur; cur = cur->_next;}//return nullptr;return{nullptr,this};}

unordered_setinsert

//bool insert(const K& key) pair<iterator,bool>insert(const K& key){return _hash.insert(key);}

unordered_mapinsertoperator[]

//bool insert(const pair<K, V>& kv) pair<iterator,bool>insert(const pair<K, V>& kv){return _hash.insert(kv);} V&operator[](const K& key){ pair<iterator,bool> p =insert({ key,V()});return p.first->second;}
这里提供了测试unordered_map的方法:
voidtest_map(){ HL::unordered_map<string, string> ump; ump.insert({"sort","排序"}); ump.insert({"left","左边"}); ump.insert({"right","右边"}); ump["left"]="左边,剩余"; ump["insert"]="插入"; ump["string"]; HL::unordered_map<string, string>::iterator it = ump.begin();while(it != ump.end()){ it->second +='x'; cout << it->first <<":"<< it->second << endl;++it;} cout << endl;}
在这里插入图片描述

可以看到没有问题的好吧

4. 构造函数和析构函数

这里我们HashTable是写了构造函数和析构函数的,而unordered_setunordered_map都只是对HashTable的封装复用,所以这里不需要写unordered_setunordered_map的析构函数的。

三、简单总结

供了测试unordered_map的方法:

voidtest_map(){ HL::unordered_map<string, string> ump; ump.insert({"sort","排序"}); ump.insert({"left","左边"}); ump.insert({"right","右边"}); ump["left"]="左边,剩余"; ump["insert"]="插入"; ump["string"]; HL::unordered_map<string, string>::iterator it = ump.begin();while(it != ump.end()){ it->second +='x'; cout << it->first <<":"<< it->second << endl;++it;} cout << endl;}

[外链图片转存中…(img-lLsjrj3p-1743398072126)]

可以看到没有问题的好吧

4. 构造函数和析构函数

这里我们HashTable是写了构造函数和析构函数的,而unordered_setunordered_map都只是对HashTable的封装复用,所以这里不需要写unordered_setunordered_map的析构函数的。

三、简单总结

这里erase函数也是直接复用HashTableerase函数,这里就不演示了。

简单总结一下

这里我们通过复用HashTable,实现了unordered_setunordered_mapinsertfind迭代器以及unordered_mapoperator[]。首先就是复用HashTable,实现unordered_setunordered_map的框架;然后实现HashTable的迭代器Iterator,并完成复用实现unordered_setunordered_mapiterator;接着就是unordered_mapoperator[]操作;最后呢就是构造函数析构函数这些(虽然不需要写unordered_setunordered_map)。

这里对于sizeempty等这些简单的方法这里并没有实现(也是直接复用的好吧

到这里本篇内容就结束了,希望对你有所帮助。

制作不易,感谢大佬的支持。

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

Read more

2026 Python+AI入门|0基础速通,吃透热门轻量化玩法

2026 Python+AI入门|0基础速通,吃透热门轻量化玩法

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:AI 文章目录: * 一、2026 Python+AI入门,必抓3个热门新趋势 * 二、入门前提:不用啃硬骨头,掌握这2点就够了 * 环境搭建(10分钟搞定,Windows/Mac通用) * 三、3个实战案例 * 案例1:30行代码开发AI文本总结工具(轻量化工具,最易上手) * 案例2:大模型微调入门(Llama 3微调,2026热门) * 案例3:AI自动数据标注(图像标注,企业刚需) * 四、Python+AI入门学习流程图(2026最新,不绕路) * 五、2026新手避坑指南 * 六、总结 【前言】 大家好,我是一名深耕AI入门教学的开发者,

By Ne0inhk
机器学习:数据清洗与预处理 | Python

机器学习:数据清洗与预处理 | Python

个人主页-爱因斯晨 文章专栏-Python学习 文章目录 * 个人主页-爱因斯晨 * 文章专栏-Python学习 * 前言 * 了解数据清洗 * 数据清洗的步骤 * 1. 环境准备与库导入 * 2. 数据加载 * 3. 数据初探与理解 * 4. 缺失值处理 * 5. 重复值处理 * 6. 异常值处理 * 7. 数据类型转换 * 8. 数据标准化 / 归一化(预处理) * 实例实践 * 总结 前言 我们不论在学习机器学习还是数据分析中,都会涉及很多数据。但原数据不可避免有很多杂志,为了确保结果的准确性,我们需要首先进行数据清洗和预处理。 了解数据清洗 数据清洗就像是一场数据的“大扫除”。它是从原始数据中找出并修正那些错误、不完整、重复或不一致的数据。通过数据清洗,能显著提升数据质量,为后续数据分析、挖掘和建模等工作提供准确、可靠、干净的数据基础,从而让基于数据得出的结论更具可信度和价值。 数据清洗的步骤 1. 环境准备与库导入

By Ne0inhk
Python 爬虫项目实战(一):爬取某云热歌榜歌曲

Python 爬虫项目实战(一):爬取某云热歌榜歌曲

前言 网络爬虫(Web Crawler),也称为网页蜘蛛(Web Spider)或网页机器人(Web Bot),是一种按照既定规则自动浏览网络并提取信息的程序。爬虫的主要用途包括数据采集、网络索引、内容抓取等。 爬虫的基本原理 1. 种子 URL:爬虫从一个或多个种子 URL 开始,这些 URL 是起点。 2. 发送请求:爬虫向这些种子 URL 发送 HTTP 请求,通常是 GET 请求。 3. 获取响应:服务器返回网页的 HTML 内容作为响应。 4. 解析内容:爬虫解析 HTML 内容,提取所需的数据(如文本、链接、图片等)。 5. 提取链接:

By Ne0inhk
JAVA 泛型与通配符:从原理到实战应用

JAVA 泛型与通配符:从原理到实战应用

JAVA 泛型与通配符:从原理到实战应用 1.1 本章学习目标与重点 💡 掌握泛型的核心概念与设计初衷,理解泛型的编译期检查机制。 💡 熟练使用泛型类、泛型接口和泛型方法,解决数据类型安全问题。 💡 理解通配符(?)、上界通配符(? extends T)和下界通配符(? super T)的使用场景。 ⚠️ 本章重点是 泛型的擦除机制 和 通配符的灵活运用,这是提升代码通用性和安全性的关键。 1.2 泛型的核心概念与设计初衷 1.2.1 为什么需要泛型 在没有泛型的 JDK 5 之前,集合类只能存储 Object 类型的对象。获取元素时需要强制类型转换,这会带来两个严重问题: 1. 类型不安全:可以向集合中添加任意类型的对象,运行时可能抛出 ClassCastException。 2. 代码臃肿:频繁的强制类型转换会让代码可读性和维护性变差。 💡 泛型的出现就是为了解决这些问题,它的核心思想是

By Ne0inhk