【C++】STL详解(十二)—— 用哈希表封装出unordered_map和unordered_set

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C++学习
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【C++】STL详解(十一)—— unordered_set、unordered_map的介绍及使用

文章目录

  • 哈希表源代码
  • 哈希表模板参数的控制
  • string类型无法取模问题
  • 哈希表默认成员函数实现
  • 哈希表正向迭代器的实现
  • unordered_set的模拟实现
  • unordered_map的模拟实现
  • 封装完成后的代码
    • 哈希表的代码
    • 正向迭代器的代码
    • unordered_set的代码
    • unordered_map的代码
  • 总结:

哈希表源代码

下面我们将对一个KV模型的哈希表进行封装,同时模拟实现出C++STL库当中的unordered_map和unordered_set,所用到的哈希表源代码如下:

//每个哈希桶中存储数据的结构
template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;//构造函数HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};//哈希表
template<class K, class V>
class HashTable
{typedef HashNode<K, V> Node; //哈希结点类型
public://获取本次增容后哈希表的大小size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;//素数序列const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}return primeList[i];}//插入函数bool Insert(const pair<K, V>& kv){//1、查看哈希表中是否存在该键值的键值对Node* ret = Find(kv.first);if (ret) //哈希表中已经存在该键值的键值对(不允许数据冗余){return false; //插入失败}//2、判断是否需要调整哈希表的大小if (_n == _table.size()) //哈希表的大小为0,或负载因子超过1{//增容//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍(若哈希表大小为0,则将哈希表的初始大小设置为10)vector<Node*> newtable;newtable.resize(GetNextPrime(_table.size()));//b、将原哈希表当中的结点插入到新哈希表for (size_t i = 0; i < _table.size(); i++){if (_table[i]) //桶不为空{Node* cur = _table[i];while (cur) //将该桶的结点取完为止{Node* next = cur->_next; //记录cur的下一个结点size_t index = cur->_kv.first%newtable.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)//将该结点头插到新哈希表中编号为index的哈希桶中cur->_next = newtable[index];newtable[index] = cur;cur = next; //取原哈希表中该桶的下一个结点}_table[i] = nullptr; //该桶取完后将该桶置空}}//c、交换这两个哈希表_table.swap(newtable);}//3、将键值对插入哈希表size_t index = kv.first % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)Node* newnode = new Node(kv); //根据所给数据创建一个待插入结点//将该结点头插到新哈希表中编号为index的哈希桶中newnode->_next = _table[index];_table[index] = newnode;//4、哈希表中的有效元素个数加一_n++;return true;}//查找函数HashNode<K, V>* Find(const K& key){if (_table.size() == 0) //哈希表大小为0,查找失败{return nullptr;}size_t index = key % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)//遍历编号为index的哈希桶HashNode<K, V>* cur = _table[index];while (cur) //直到将该桶遍历完为止{if (cur->_kv.first == key) //key值匹配,则查找成功{return cur;}cur = cur->_next;}return nullptr; //直到该桶全部遍历完毕还没有找到目标元素,查找失败}//删除函数bool Erase(const K& key){//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)size_t index = key % _table.size();//2、在编号为index的哈希桶中寻找待删除结点Node* prev = nullptr;Node* cur = _table[index];while (cur) //直到将该桶遍历完为止{if (cur->_kv.first == key) //key值匹配,则查找成功{//3、若找到了待删除结点,则删除该结点if (prev == nullptr) //待删除结点是哈希桶中的第一个结点{_table[index] = cur->_next; //将第一个结点从该哈希桶中移除}else //待删除结点不是哈希桶的第一个结点{prev->_next = cur->_next; //将该结点从哈希桶中移除}delete cur; //释放该结点//4、删除结点后,将哈希表中的有效元素个数减一_n--;return true; //删除成功}prev = cur;cur = cur->_next;}//假删除可能会导致迭代器失效return false; //直到该桶全部遍历完毕还没有找到待删除元素,删除失败}
private:vector<Node*> _table; //哈希表size_t _n = 0; //哈希表中的有效元素个数
};

哈希表模板参数的控制

首先需要明确的是,unordered_set是K模型的容器,而unordered_map是KV模型的容器。

要想只用一份哈希表代码同时封装出K模型和KV模型的容器,我们必定要对哈希表的模板参数进行控制。

为了与原哈希表的模板参数进行区分,这里将哈希表的第二个模板参数的名字改为T。

template<class K, class T>
class HashTable

如果上层使用的是unordered_set容器,那么传入哈希表的模板参数就是key和key。

template<class K>
class unordered_set
{
public://...
private:HashTable<K, K> _ht; //传入底层哈希表的是K和K
};

但如果上层使用的是unordered_map容器,那么传入哈希表的模板参数就是key以及key和value构成的键值对。

template<class K, class V>
class unordered_map
{
public://...
private:HashTable<K, pair<K, V>> _ht; //传入底层哈希表的是K以及K和V构成的键值对
};

也就是说,哈希表中的模板参数T的类型到底是什么,完全却决于上层所使用容器的种类。

而哈希结点的模板参数也应该由原来的K、V变为T:

上层容器是unordered_set时,传入的T是键值,哈希结点中存储的就是键值。
上层容器是unordered_map时,传入的T是键值对,哈希结点中存储的就是键值对。

更改模板参数后,哈希结点的定义如下:

//哈希结点的定义
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;//构造函数HashNode(const T& data):_data(data), _next(nullptr){}
};

在哈希映射过程中,我们需要获得元素的键值,然后通过哈希函数计算出对应的哈希地址进行映射。

现在由于我们在哈希结点当中存储的数据类型是T,这个T可能就是一个键值,也可能是一个键值对,对于底层的哈希表来说,它并不知道哈希结点当中存储的数据究竟是什么类型,因此需要由上层容器提供一个仿函数,用于获取T类型数据当中的键值。

因此,unordered_map容器需要向底层哈希表提供一个仿函数,该仿函数返回键值对当中的键值。

template<class K, class V>
class unordered_map
{//仿函数struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值key{return kv.first;}};
public://...
private:HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};

而虽然unordered_set容器传入哈希表的T就是键值,但是底层哈希表并不知道上层容器的种类,底层哈希表在获取键值时会统一通过传入的仿函数进行获取,因此unordered_set容器也需要向底层哈希表提供一个仿函数。

template<class K>
class unordered_set
{//仿函数struct SetKeyOfT{const K& operator()(const K& key) //返回键值key{return key;}};
public://...
private:HashTable<K, K, SetKeyOfT> _ht;
};

因此,底层哈希表的模板参数现在需要增加一个,用于接收上层容器提供的仿函数。

template<class K, class T, class KeyOfT>
class HashTable

string类型无法取模问题

字符串无法取模,是哈希问题中最常见的问题。

经过上面的分析后,我们让哈希表增加了一个模板参数,此时无论上层容器是unordered_set还是unordered_map,我们都能够通过上层容器提供的仿函数获取到元素的键值。

但是在我们日常编写的代码中,用字符串去做键值key是非常常见的事,比如我们用unordered_map容器统计水果出现的次数时,就需要用各个水果的名字作为键值。

而字符串并不是整型,也就意味着字符串不能直接用于计算哈希地址,我们需要通过某种方法将字符串转换成整型后,才能代入哈希函数计算哈希地址。

但遗憾的是,我们无法找到一种能实现字符串和整型之间一对一转换的方法,因为在计算机中,整型的大小是有限的,比如用无符号整型能存储的最大数字是4294967295,而众多字符能构成的字符串的种类却是无限的。

鉴于此,无论我们用什么方法将字符串转换成整型,都会存在哈希冲突,只是产生冲突的概率不同而已。

经过前辈们实验后发现,BKDRHash算法无论是在实际效果还是编码实现中,效果都是最突出的。该算法由于在Brian Kernighan与Dennis Ritchie的《The C Programing Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的hash算法

因此,现在我们需要在哈希表的模板参数中再增加一个仿函数,用于将键值key转换成对应的整型。

template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable

若是上层没有传入该仿函数,我们则使用默认的仿函数,该默认仿函数直接返回键值key即可,但是用字符串作为键值key是比较常见的,因此我们可以针对string类型写一个类模板的特化,此时当键值key为string类型时,该仿函数就会根据BKDRHash算法返回一个对应的整型。

template<class K>
struct Hash
{size_t operator()(const K& key) //返回键值key{return key;}
};
//string类型的特化
template<>
struct Hash<string>
{size_t operator()(const string& s) //BKDRHash算法{size_t value = 0;for (auto ch : s){value = value * 131 + ch;}return value;}
};

哈希表默认成员函数实现

一、构造函数
哈希表中有两个成员变量,当我们实例化一个对象时:

_table会自动调用vector的默认构造函数进行初始化。
_n会根据我们所给的缺省值被设置为0。

vector<Node*> _table; //哈希表
size_t _n = 0; //哈希表中的有效元素个数

因此我们不需要编写构造函数,使用默认生成的构造函数就足够了,但是由于我们后面需要编写拷贝构造函数,编写了拷贝构造函数后,默认的构造函数就不会生成了,此时我们需要使用default关键字显示指定生成默认构造函数。

//构造函数
HashTable() = default; //显示指定生成默认构造函数

二、拷贝构造函数
哈希表在拷贝时需要进行深拷贝,否则拷贝出来的哈希表和原哈希表中存储的都是同一批结点。

哈希表的拷贝构造函数实现逻辑如下:

将哈希表的大小调整为ht._table的大小。
将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中。
更改哈希表当中的有效数据个数。

//拷贝构造函数
HashTable(const HashTable& ht)
{//1、将哈希表的大小调整为ht._table的大小_table.resize(ht._table.size());//2、将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中(深拷贝)for (size_t i = 0; i < ht._table.size(); i++){if (ht._table[i]) //桶不为空{Node* cur = ht._table[i];while (cur) //将该桶的结点取完为止{Node* copy = new Node(cur->_data); //创建拷贝结点//将拷贝结点头插到当前桶copy->_next = _table[i];_table[i] = copy;cur = cur->_next; //取下一个待拷贝结点}}}//3、更改哈希表当中的有效数据个数_n = ht._n;
}

三、赋值运算符重载函数
实现赋值运算符重载函数时,可以通过参数间接调用拷贝构造函数,之后将拷贝构造出来的哈希表和当前哈希表的两个成员变量分别进行交换即可,当赋值运算符重载函数调用结束后,拷贝构造出来的哈希表会因为出了作用域而被自动析构,此时原哈希表之前的数据也就顺势被释放了。

//赋值运算符重载函数
HashTable& operator=(HashTable ht)
{//交换哈希表中两个成员变量的数据_table.swap(ht._table);swap(_n, ht._n);return *this; //支持连续赋值
}

四、析构函数
因为哈希表当中存储的结点都是new出来的,因此在哈希表被析构时必须进行结点的释放。在析构哈希表时我们只需要依次取出非空的哈希桶,遍历哈希桶当中的结点并进行释放即可。

//析构函数
~HashTable()
{//将哈希表当中的结点一个个释放for (size_t i = 0; i < _table.size(); i++){if (_table[i]) //桶不为空{Node* cur = _table[i];while (cur) //将该桶的结点取完为止{Node* next = cur->_next; //记录下一个结点delete cur; //释放结点cur = next;}_table[i] = nullptr; //将该哈希桶置空}}
}

哈希表正向迭代器的实现

哈希表的正向迭代器实际上就是对哈希结点指针进行了封装,但是由于在实现++运算符重载时,可能需要在哈希表中去寻找下一个非空哈希桶,因此每一个正向迭代器中都应该存储哈希表的地址。

//正向迭代器
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
struct __HTIterator
{typedef HashNode<T> Node; //哈希结点的类型typedef HashTable<K, T, KeyOfT, HashFunc> HT; //哈希表的类型typedef __HTIterator<K, T, KeyOfT, HashFunc> Self; //正向迭代器的类型Node* _node; //结点指针HT* _pht; //哈希表的地址
};

因此在构造正向迭代器时,我们不仅需要对应哈希结点的指针,还需要该哈希结点所在哈希表的地址。

//构造函数
__HTIterator(Node* node, HT* pht):_node(node) //结点指针, _pht(pht) //哈希表地址
{}

当对正向迭代器进行解引用操作时,我们直接返回对应结点数据的有引用即可。

T& operator*()
{return _node->_data; //返回哈希结点中数据的引用
}

当对正向迭代器进行->操作时,我们直接返回对应结点数据的地址即可。

T* operator->()
{return &_node->_data; //返回哈希结点中数据的地址
}

当我们需要比较两个迭代器是否相等时,只需要判断这两个迭代器所封装的结点是否是同一个即可。

bool operator!=(const Self& s) const
{return _node != s._node; //判断两个结点的地址是否不同
}bool operator==(const Self& s) const
{return _node == s._node; //判断两个结点的地址是否相同
}

++运算符重载函数的实现逻辑并不是很难,我们只需要知道如何找到当前结点的下一个结点即可。

若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点。
若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点。

//前置++
Self& operator++()
{if (_node->_next) //该结点不是当前哈希桶中的最后一个结点{_node = _node->_next; //++后变为当前哈希桶中的下一个结点}else //该结点是当前哈希桶中的最后一个结点{KeyOfT kot;HashFunc hf;size_t index = hf(kot(_node->_data)) % _pht->_table.size(); //通过哈希函数计算出当前所处哈希桶编号index(除数不能是capacity)index++; //从下一个位置开始找一个非空的哈希桶while (index < _pht->_table.size()) //直到将整个哈希表找完{if (_pht->_table[index]) //当前哈希桶非空{_node = _pht->_table[index]; //++后变为当前哈希桶中的第一个结点return *this;}index++; //当前哈希桶为空桶,找下一个哈希桶}_node = nullptr; //哈希表中已经没有空桶了,++后变为nullptr}return *this;
}

注意: 哈希表的迭代器类型是单向迭代器,没有反向迭代器,即没有实现–运算符的重载,若是想让哈希表支持双向遍历,可以考虑将哈希桶中存储的单链表结构换为双链表结构。

哈希表结构的其他实现方式

在其他地方可能将插入哈希表的哈希结点统一链接到同一个单链表上,此时实现哈希表的正向迭代器时就更简单了,实现++运算符重载时,想要找到下一个结点就直接通过当前结点就可以找到。

正向迭代器实现后,我们需要在哈希表的实现当中进行如下操作:

进行正向迭代器类型的typedef,需要注意的是,为了让外部能够使用typedef后的正向迭代器类型iterator,我们需要在public区域进行typedef。
由于正向迭代器中++运算符重载函数在寻找下一个结点时,会访问哈希表中的成员变量_table,而_table成员变量是哈希表的私有成员,因此我们需要将正向迭代器类声明为哈希表类的友元。
将哈希表中查找函数返回的结点指针,改为返回由结点指针和哈希表地址构成的正向迭代器。
将哈希表中插入函数的返回值类型,改为由正向迭代器类型和布尔类型所构成的键值对。

然后我们就可以在哈希表中实现迭代器相关的成员函数了:

begin函数: 返回哈希表中第一个非空哈希桶中的第一个结点的正向迭代器。
end函数: 返回空指针的正向迭代器。

//哈希表的实现
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable
{//将正向迭代器类声明为哈希表类的友元template<class K, class T, class KeyOfT, class HashFunc>friend struct __HTIterator;typedef HashNode<T> Node; //哈希结点类型
public:typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator; //正向迭代器的类型iterator begin(){size_t i = 0;while (i < _table.size()) //找到第一个非空哈希桶{if (_table[i]) //该哈希桶非空{return iterator(_table[i], this); //返回该哈希桶中的第一个结点的正向迭代器}i++;}return end(); //哈希桶中无数据,返回end()}iterator end(){return iterator(nullptr, this); //返回nullptr的正向迭代器}
private:vector<Node*> _table; //哈希表size_t _n = 0; //哈希表中的有效元素个数
};

unordered_set的模拟实现

实现unordered_set的各个接口时,就只需要调用底层哈希表对应的接口就行了。

template<class K>
class unordered_set
{//仿函数struct SetKeyOfT{const K& operator()(const K& key) //返回键值key{return key;}};
public://现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取typedef typename HashTable<K, K, SetKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}//插入函数pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}//删除函数void erase(const K& key){_ht.Erase(key);}//查找函数iterator find(const K& key){return _ht.Find(key);}
private:HashTable<K, K, SetKeyOfT> _ht;
};

unordered_map的模拟实现

实现unordered_map的各个接口时,也是调用底层哈希表对应的接口就行了,此外还需要实现[ ]运算符的重载。

template<class K, class V>
class unordered_map
{//仿函数struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值key{return kv.first;}};
public:typedef typename HashTable<K, pair<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 = insert(make_pair(key, V()));iterator it = ret.first;return it->second;}//删除函数void erase(const K& key){_ht.Erase(key);}//查找函数iterator find(const K& key){return _ht.Find(key);}
private:HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};

封装完成后的代码

哈希表的代码

//哈希结点的定义
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;//构造函数HashNode(const T& data):_data(data), _next(nullptr){}
};
template<class K>
struct Hash
{size_t operator()(const K& key) //返回键值key{return key;}
};
//string类型的特化
template<>
struct Hash<string>
{size_t operator()(const string& s) //BKDRHash算法{size_t value = 0;for (auto ch : s){value = value * 131 + ch;}return value;}
};
//哈希表的实现
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable
{//将正向迭代器类声明为哈希表类的友元template<class K, class T, class KeyOfT, class HashFunc>friend struct __HTIterator;//friend struct __HTIterator<K, T,KeyOfT, HashFunc>;typedef HashNode<T> Node; //哈希结点类型
public:typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator; //正向迭代器的类型iterator begin(){size_t i = 0;while (i < _table.size()) //找到第一个非空哈希桶{if (_table[i]) //该哈希桶非空{return iterator(_table[i], this); //返回该哈希桶中的第一个结点的正向迭代器}i++;}return end(); //哈希桶中无数据,返回end()}iterator end(){return iterator(nullptr, this); //返回nullptr的正向迭代器}//构造函数HashTable() = default; //显示指定生成默认构造//拷贝构造函数HashTable(const HashTable& ht){//1、将哈希表的大小调整为ht._table的大小_table.resize(ht._table.size());//2、将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中(深拷贝)for (size_t i = 0; i < ht._table.size(); i++){if (ht._table[i]) //桶不为空{Node* cur = ht._table[i];while (cur) //将该桶的结点取完为止{Node* copy = new Node(cur->_data); //创建拷贝结点//将拷贝结点头插到当前桶copy->_next = _table[i];_table[i] = copy;cur = cur->_next; //取下一个待拷贝结点}}}//3、更改哈希表当中的有效数据个数_n = ht._n;}//赋值运算符重载函数HashTable& operator=(HashTable ht){//交换哈希表中两个成员变量的数据_table.swap(ht._table);swap(_n, ht._n);return *this; //支持连续赋值}//析构函数~HashTable(){//将哈希表当中的结点一个个释放for (size_t i = 0; i < _table.size(); i++){if (_table[i]) //桶不为空{Node* cur = _table[i];while (cur) //将该桶的结点取完为止{Node* next = cur->_next; //记录下一个结点delete cur; //释放结点cur = next;}_table[i] = nullptr; //将该哈希桶置空}}}//获取本次增容后哈希表的大小size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;//素数序列const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}return primeList[i];}//插入函数pair<iterator, bool> Insert(const T& data){KeyOfT kot;//1、查看哈希表中是否存在该键值的键值对iterator ret = Find(kot(data));if (ret != end()) //哈希表中已经存在该键值的键值对(不允许数据冗余){return make_pair(ret, false); //插入失败}//2、判断是否需要调整哈希表的大小if (_n == _table.size()) //哈希表的大小为0,或负载因子超过1{//增容//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍(若哈希表大小为0,则将哈希表的初始大小设置为10)HashFunc hf;vector<Node*> newtable;//size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;//newtable.resize(newsize);newtable.resize(GetNextPrime(_table.size()));//b、将原哈希表当中的结点插入到新哈希表for (size_t i = 0; i < _table.size(); i++){if (_table[i]) //桶不为空{Node* cur = _table[i];while (cur) //将该桶的结点取完为止{Node* next = cur->_next; //记录cur的下一个结点size_t index = hf(kot(cur->_data))%newtable.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)//将该结点头插到新哈希表中编号为index的哈希桶中cur->_next = newtable[index];newtable[index] = cur;cur = next; //取原哈希表中该桶的下一个结点}_table[i] = nullptr; //该桶取完后将该桶置空}}//c、交换这两个哈希表_table.swap(newtable);}//3、将键值对插入哈希表HashFunc hf;size_t index = hf(kot(data)) % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)Node* newnode = new Node(data); //根据所给数据创建一个待插入结点//将该结点头插到新哈希表中编号为index的哈希桶中newnode->_next = _table[index];_table[index] = newnode;//4、哈希表中的有效元素个数加一_n++;return make_pair(iterator(newnode, this), true);}//查找函数iterator Find(const K& key){if (_table.size() == 0) //哈希表大小为0,查找失败{return end();}KeyOfT kot;HashFunc hf;size_t index = hf(key) % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)//遍历编号为index的哈希桶HashNode<T>* cur = _table[index];while (cur) //直到将该桶遍历完为止{if (kot(cur->_data) == key) //key值匹配,则查找成功{return iterator(cur, this);}cur = cur->_next;}return end(); //直到该桶全部遍历完毕还没有找到目标元素,查找失败}//删除函数bool Erase(const K& key){KeyOfT kot;HashFunc hf;//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)size_t index = hf(key) % _table.size();//2、在编号为index的哈希桶中寻找待删除结点Node* prev = nullptr;Node* cur = _table[index];while (cur) //直到将该桶遍历完为止{if (kot(cur->_data) == key) //key值匹配,则查找成功{//3、若找到了待删除结点,则删除该结点if (prev == nullptr) //待删除结点是哈希桶中的第一个结点{_table[index] = cur->_next; //将第一个结点从该哈希桶中移除}else //待删除结点不是哈希桶的第一个结点{prev->_next = cur->_next; //将该结点从哈希桶中移除}delete cur; //释放该结点//4、删除结点后,将哈希表中的有效元素个数减一_n--;return true; //删除成功}prev = cur;cur = cur->_next;}//假删除可能会导致迭代器失效return false; //直到该桶全部遍历完毕还没有找到待删除元素,删除失败}
private:vector<Node*> _table; //哈希表size_t _n = 0; //哈希表中的有效元素个数
};

正向迭代器的代码

//前置声明
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;//正向迭代器
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
struct __HTIterator
{typedef HashNode<T> Node; //哈希结点的类型typedef HashTable<K, T, KeyOfT, HashFunc> HT; //哈希表的类型typedef __HTIterator<K, T, KeyOfT, HashFunc> Self; //正向迭代器的类型Node* _node; //结点指针HT* _pht; //哈希表的地址//构造函数__HTIterator(Node* node, HT* pht):_node(node) //结点指针, _pht(pht) //哈希表地址{}T& operator*(){return _node->_data; //返回哈希结点中数据的引用}T* operator->(){return &_node->_data; //返回哈希结点中数据的地址}bool operator!=(const Self& s) const{return _node != s._node; //判断两个结点的地址是否不同}bool operator==(const Self& s) const{return _node == s._node; //判断两个结点的地址是否相同}//前置++Self& operator++(){if (_node->_next) //该结点不是当前哈希桶中的最后一个结点{_node = _node->_next; //++后变为当前哈希桶中的下一个结点}else //该结点是当前哈希桶中的最后一个结点{KeyOfT kot;HashFunc hf;size_t index = hf(kot(_node->_data)) % _pht->_table.size(); //通过哈希函数计算出当前所处哈希桶编号index(除数不能是capacity)index++; //从下一个位置开始找一个非空的哈希桶while (index < _pht->_table.size()) //直到将整个哈希表找完{if (_pht->_table[index]) //当前哈希桶非空{_node = _pht->_table[index]; //++后变为当前哈希桶中的第一个结点return *this;}index++; //当前哈希桶为空桶,找下一个哈希桶}_node = nullptr; //哈希表中已经没有空桶了,++后变为nullptr}return *this;}
};

unordered_set的代码

namespace sherry //防止命名冲突
{template<class K>class unordered_set{//仿函数struct SetKeyOfT{const K& operator()(const K& key) //返回键值key{return key;}};public://现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取typedef typename HashTable<K, K, SetKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}//插入函数pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}//删除函数void erase(const K& key){_ht.Erase(key);}//查找函数iterator find(const K& key){return _ht.Find(key);}private:HashTable<K, K, SetKeyOfT> _ht;};
}

unordered_map的代码

namespace sherry //防止命名冲突
{template<class K, class V>class unordered_map{//仿函数struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值key{return kv.first;}};public:typedef typename HashTable<K, pair<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 = insert(make_pair(key, V()));iterator it = ret.first;return it->second;}//删除函数void erase(const K& key){_ht.Erase(key);}//查找函数iterator find(const K& key){return _ht.Find(key);}private:HashTable<K, pair<K, V>, MapKeyOfT> _ht;};
}

总结:

今天我们比较详细地完成了用一个哈希表同时封装出unordered_map和unordered_set,了解了一些有关的底层原理。接下来,我们将进行STL中bitset类的学习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/150478.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云ECS服务器上启动的portainer无法访问的问题

如下图&#xff0c;在阿里云ECS服务器上安装并启动了portainer&#xff0c;但是在自己电脑上访问不了远程的portainer。 最后发现是要在网络安全组里开放9000端口号&#xff0c;具体操作如下&#xff1a; 在云服务器管理控制台点击左侧菜单中的网络与安全-安全组&#xff0c;然…

黑豹程序员-架构师学习路线图-百科:Database数据库

文章目录 1、什么是Database2、发展历史3、数据库排行网4、总结 1、什么是Database 当今世界是一个充满着数据的互联网世界&#xff0c;各处都充斥着大量的数据。即这个互联网世界就是数据世界。 支撑这个数据世界的基石就是数据库&#xff0c;数据库也可以称为数据的仓库。 …

基于虚拟同步发电机控制的双机并联Simulink仿真模型

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【Redis】五大数据类型 、历史概述、nosql分类

文章目录 NoSql概述NoSql年代缓存 Memcached MySQL垂直拆分&#xff08;读写分离&#xff09;分库分表水平拆分Mysql集群最近为什么要用 NoSqlNoSql的四大分类 Redis测试性能 五大数据类型keyStringSetHashZset 前言&#xff1a;本文为看狂神视频记录的笔记 NoSql概述 NoSql年…

一篇理解http协议

一、http协议。 HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是一种在Web中广泛使用的应用层协议&#xff0c;它定义了客户端和服务器之间的通信规则&#xff0c;简化了Web应用程序的开发和交互过程。其实传输是由TCP协议完成的。 HT…

idea Springboot 图书管理系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 springboot 图书管理系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#…

[NISACTF 2022]popchains - 反序列化+伪协议

[NISACTF 2022]popchains 一、解题流程二、小小疑惑 一、解题流程 1、链条&#xff1a;Road_is_Long&#xff08;construct->wakeup【page$r】-> toString【string$m】&#xff09;-> Make_a_Change&#xff08;construct->get【effort$t】&#xff09;-> Try_W…

基于Springboot实现简历管理系统演示【项目源码+论文说明】分享

基于Springboot实现简历管理系统演示 摘要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;简历系统当然也不能排除在外。简历系统是以实际运用为开发背景&#xff0c;运用软件…

想要精通算法和SQL的成长之路 - 并查集的运用和案例(省份数量)

想要精通算法和SQL的成长之路 - 并查集的运用 前言一. 并查集的使用和模板1.1 初始化1.2 find 查找函数1.3 union 合并集合1.4 connected 判断相连性1.5 完整代码 二. 运用案例 - 省份数量 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 并查集的使用和模板 先说一下并查集…

Qt之显示PDF文件

之前使用过mupdf库&#xff0c;能够成功显示pdf&#xff0c;但是我用着有BUG&#xff0c;不太理解它的代码&#xff0c;搞了好久都不行。后面又试了其他库&#xff0c;如pdfium、popler、下载了很多例程&#xff0c;都跑不起来&#xff01;后面偶然得知xpdf库&#xff0c;看起来…

蛋仔派对如何获得蛋币,蛋仔派对怎么切换账号

在蛋仔派对游戏中&#xff0c;蛋币是一种虚拟货币&#xff0c;用以购买游戏道具或提升游戏体验。以下是五种可能的获得蛋币的方式&#xff1a; 关注【娱乐天梯】&#xff0c;获取内部福利号 1. 完成挑战和任务&#xff1a;玩家可以通过完成不同类型的任务和挑战来获取蛋币。任务…

基于粒子群优化算法、鲸鱼算法、改进的淘沙骆驼模型算法(PSO/SSA/tGSSA)的微电网优化调度(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

QT之可自由折叠和展开的布局

介绍和功能分析 主要是实现控件的折叠和展开&#xff0c;类似抽屉控件&#xff0c;目前Qt自带的控件QToolBox具有这个功能&#xff0c;但是一次只能展开一个&#xff0c;所以针对自己的需求可以自己写一个类似的功能&#xff0c;这里实现的方法比较多&#xff0c;其实原理也比较…

【技术干货】如何通过 DP 实现支持经典蓝牙的联网单品设备与 App 配对

经典蓝牙模块&#xff08;Classic Bluetooth&#xff09;主要用于呼叫和音频传输&#xff0c;所以经典蓝牙最主要的特点就是功耗大&#xff0c;传输数据量大。蓝牙耳机、蓝牙音箱等场景大多采用经典蓝牙&#xff0c;因为蓝牙是为传输声音而设计的&#xff0c;是短距离音频传输的…

数据中台实战(11)-数据中台的数据安全解决方案

0 微盟删库跑路 除了快、准和省&#xff0c;数据中台须安全&#xff0c;避免“微盟删库跑路”。 2020年2月23日19点&#xff0c;国内最大精准营销服务商微盟出现大面积系统故障&#xff0c;旗下300万商户线上业务全停&#xff0c;商铺后台所有数据被清。始作俑者是一位运维&a…

[引擎开发] 杂谈ue4中的Vulkan

接触Vulkan大概也有大半年&#xff0c;概述一下自己这段时间了解到的东西。本文实际上是杂谈性质而非综述性质&#xff0c;带有严重的主观认知&#xff0c;因此并没有那么严谨。 使用Vulkan会带来什么呢&#xff1f;简单来说就是对底层更好的控制。这意味着我们能够有更多的手段…

王道考研计算机网络——传输层

一、传输层概述 复用&#xff1a;发送方不同的应用进程都可以使用同一个传输层的协议来传送数据 分用&#xff1a;接收方的传输层在去除报文段的首部之后能把数据交给正确的应用进程 熟知端口号就是知名端口号0-1023 客户端使用的端口号是动态变化的&#xff0c;不是唯一确定…

让照片人物开口说话,SadTalker 安装及使用(避坑指南)

AI技术突飞猛进&#xff0c;不断的改变着人们的工作和生活。数字人直播作为新兴形式&#xff0c;必将成为未来趋势&#xff0c;具有巨大的、广阔的、惊人的市场前景。它将不断融合创新技术和跨界合作&#xff0c;提供更具个性化和多样化的互动体验&#xff0c;成为未来的一种趋…

010:连续跌3天,同时这三天收盘价都在20日均线下,第四天上涨的概率--以京泉华为例

对于《连续跌三天&#xff0c;压第四天上涨的盈利计算》&#xff0c;我们可以继续优化这个策略&#xff0c;增加条件&#xff1a;同时三天都收盘在20日均线下。 因为我们上一篇《获取20日均线数据到excel表中》获得了20日均线数据&#xff0c;我们可以利用均线数据来编写新的脚…