【哈希表封装实现】—— 我与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

【C++】类和对象—(下) 收官之战

【C++】类和对象—(下) 收官之战

前言:上一篇文章我们向大家介绍了类和对象的核心六个成员函数中的4个,其余两个以及初始化列表,static成员,内部类,匿名对象等会在本篇文章介绍! ✨ 坚持用清晰易懂的图解+代码语言, 让每个知识点都简单直观! 🚀 个人主页 :MSTcheng · ZEEKLOG 🌱 代码仓库 :MSTcheng · Gitee 📌 专栏系列 :📖 《C语言》🧩 《数据结构》💡 《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!” 文章目录 * 一,运算符重载 * 1.1什么是运算符重载? * 1.2 为什么要创造运算符重载? * 二,赋值运算符重载 * 2.1赋值运算符重载的构成 * 2.1 >>流插入<<流提取重载 * 3.1const成员函数 * 4.1取地址运算符重载 * 三,初始化列表 * 3.

By Ne0inhk

为什么顶尖团队都在用Concepts简化C++元编程?真相在这里

第一章:为什么顶尖团队都在用Concepts简化C++元编程? C++20 引入的 Concepts 彻底改变了模板元编程的编写方式,让类型约束从“运行时错误”转向“编译时契约”。传统模板依赖 SFINAE 或 requires 表达式进行类型检查,代码冗长且难以维护。而 Concepts 提供了一种清晰、可读性强的语法,使开发者能直接声明模板参数的语义要求。 更直观的类型约束 使用 Concepts 可以定义可重用的约束条件,提升模板接口的可读性与安全性。例如,定义一个适用于所有可加类型的操作: template concept Addable = requires(T a, T b) { { a + b } -> std::same_as; }; template T add(T a,

By Ne0inhk
特殊类的设计----《Hello C++ Wrold!》(28)--(C/C++)

特殊类的设计----《Hello C++ Wrold!》(28)--(C/C++)

文章目录 * 前言 * 设计一个不能被拷贝的类 * 设计一个只能在堆上创建对象的类 * 设计一个只能在栈上创建对象的类 * 设计一个不能被继承的类 * 设计一个只能创建一个对象的类(也叫做单例模式) * 单例模式的两种实现方法 * 饿汉模式 * 懒汉模式 前言 在 C++ 面向对象编程体系中,类是封装数据与行为的核心单元,其设计直接关系到程序的安全性、可维护性与性能表现。除了支撑常规业务逻辑的普通类,实际开发中常需面对具有特殊约束的场景:例如防止对象拷贝以规避资源重复释放风险,限定对象创建位置(仅堆或仅栈)以规范内存管理,禁止类被继承以保障核心逻辑不被篡改,或是确保类仅存在一个实例以实现全局资源统一调度 —— 这些需求的实现,正是特殊类设计的核心范畴。 本文聚焦 “特殊类设计” 这一主题,系统拆解五种典型特殊类的实现逻辑与技术细节。从 “不能被拷贝的类” 对拷贝构造函数、赋值运算符的管控,到 “只能在堆 / 栈上创建对象的类” 对构造函数与内存分配接口的限制;从 “不能被继承的类” 基于构造函数私有化(C++98)与final关键字(

By Ne0inhk
嘿嘿 解决了Dev C++ 中文乱码(有效版)

嘿嘿 解决了Dev C++ 中文乱码(有效版)

这是博主第一篇博客!记录一下博主的小小小小解决史! 很早就下载用了Dev c++ ,但现在隔了很长时间没去用过了再次打开发现出现中文乱码的现象!在网站上翻阅了许久!终于解决了问题!困扰了许久! ——————————————————————— 这个中文乱码看着是真烦得慌!!! tips:不要急不要急,事情慢慢都能解决掉滴! 还有不要保存在C盘哦!最好都保存在D盘内!本博客示范的未命名1.c 保存于C盘桌面上是为了演示方便! ———————————————————————————  图1 这是我们原来出现中文乱码的界面 编译的时候会出现这个窗口   图一 (再说一遍!这个中文乱码在之前没解决掉问题的时候一看到这个就很烦! ) 图二是编译过后(中文乱码版) 图二          ————————————————————————— 第一种方法(也是博主强推亲测有效法) ·第一步         请点击左上角<控制台界面>左上角                选中<默认值D> 图三   操作第一步     ·第二步          将下方“使用旧版本

By Ne0inhk