【STL】用哈希表(桶)封装出unordered_set和unordered_map

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++进阶
⭐代码仓库:C++进阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!

用哈希表(桶)封装出unordered_set和unordered_map

  • 一、所用的哈希代码
  • 二、哈希模板参数
    • 1、T模板参数
    • 2、仿函数
    • 3、string类型无法取模
  • 三、哈希表成员函数
    • 1、构造函数
    • 2、拷贝构造函数
    • 3、赋值运算符重载
    • 4、析构函数
    • 5、哈希桶的正向迭代器
      • 搭个框架
      • 构造函数
      • 正向迭代器解引用操作
      • 正向迭代器指针指向操作
      • 判断两个正向迭代器是否相等
      • operator++运算符
    • 6、const迭代器
    • 7、哈希表中的操作
      • (1)begin() 和 end()
      • (2)GetNextPrime
      • (3)Find函数修改
      • (4)Insert函数修改
      • (5)Erase函数修改
  • 四、Unordered_set和Unordered_map封装
  • 五、代码汇总
    • HashTable.h
    • Unordered_map.h
    • Unordered_set.h


一、所用的哈希代码

我们要实现KV模型的哈希并且进行封装unordered_set和unordered_map,那么我们就用哈希桶来解决这个问题。

#include<vector>
#include<string>
using namespace std;namespace HashT
{// 将kv.first强转成size_t// 仿函数template<class K>struct DefaultHashTable{size_t operator()(const K& key){return (size_t)key;}};// 模版的特化(其他类型) template<>struct DefaultHashTable<string>{size_t operator()(const string& str){// BKDRsize_t hash = 0;for (auto ch : str){// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等hash *= 131;hash += ch;}return hash;}};template<class K, class V>struct HashNode{HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}pair<K, V> _kv;HashNode<K, V>* _next;};//哈希表template<class K, class V, class HashFunc = DefaultHashTable<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_table.resize(10, nullptr);}// 插入bool Insert(const pair<K, V>& kv){// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整// 3将键值对插入哈希表// 4哈希表中的有效元素个数加一HashFunc hf;// 1、查看键值对,调用Find函数Node* key_ = Find(kv.first);if (key_) // 如果key_是存在的则插入不了{return false; // 插入不了}// 2、判断负载因子,负载因子是1的时候进行增容if (_n == _table.size()) // 整个哈希表都已经满了{// 增容// a.创建一个新表,将原本容量扩展到原始表的两倍int HashiNewSize = _table.size() * 2;vector<Node*> NewHashiTable;NewHashiTable.resize(HashiNewSize, nullptr);// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中for (size_t i = 0; i < _table.size(); ++i){if (_table[i]) // 这个桶中的有数据/链表存在{Node* cur = _table[i]; // 记录头结点while (cur){Node* next = cur->_next; // 记录下一个结点size_t hashi = hf(kv.first) % _table.size(); // 记录一下新表的位置// 头插到新表中cur->_next = _table[hashi];_table[hashi] = cur;cur = next; // 哈希这个桶的下一个结点}_table[i] = nullptr;}}// c.交换两个表_table.swap(NewHashiTable);}// 3、将键值对插入到哈希表中size_t hashii = hf(kv.first) % _table.size();Node* newnode = new Node(kv);// 头插法newnode->_next = _table[hashii];_table[hashii] = newnode;// 4、将_n++++_n;return true;}// 查找HashNode<K, V>* Find(const K& key){//1通过哈希函数计算出对应的哈希地址//2通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可HashFunc hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi]; // 刚好到哈希桶的位置while (cur){// 找到匹配的了if (cur->_kv.first == key){return (HashNode<K, V>*)cur;}cur = cur->_next;}return nullptr;}// 删除bool Erase(const K& key){//1通过哈希函数计算出对应的哈希桶编号//2遍历对应的哈希桶,寻找待删除结点//3若找到了待删除结点,则将该结点从单链表中移除并释放//4删除结点后,将哈希表中的有效元素个数减一HashFunc hf;size_t hashi = hf(key) % _table.size();// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点// cur记录的是当前结点,当然是要删除的结点Node* prev = nullptr;Node* cur = _table[hashi];while (cur) // 遍历到结尾{if (cur->_kv.first == key) // 刚好找到这个值{// 第一种情况:这个要删除的值刚好是哈希桶的头结点if (prev == nullptr){_table[hashi] = cur->_next;}// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值else // (prev != nullptr){prev->_next = cur->_next;}delete cur; // 删除cur结点_n--;return true;}prev = cur;cur = cur->_next;}return false;}public:// 打印一下void Print(){// 大循环套小循环for (size_t i = 0; i < _table.size(); ++i){printf("[%d]->", i);Node* cur = _table[i];// 小循环while (cur){cout << cur->_kv.first << ":" << cur->_kv.second << "->";cur = cur->_next;}printf("NULL\n");}}private:vector<Node*> _table; //哈希表size_t _n = 0; //哈希表中的有效元素个数};
}

二、哈希模板参数

我们根据之前所学习的内容,明白unordered_set是K模型,unordered_map是KV模型,而我们想要实现一个哈希桶封装这两个容器,那必然需要牺牲一个容器的利益,那么就是牺牲set的利益,让set跟着map来,大不了两个模版参数一样不就好了吗,ok!

1、T模板参数

我们为了和哈希表原函数的模板做区分,所以我们将hash的第二个参数变成T模版:

在这里插入图片描述

而我们的上层使用的是unordered_set的话,我们就是K模型,那么我们传入hash中应该是key,key模型,如下:

template<class K>
class Unordered_Set
{
public:private:HashT::HashTable<K, K> _ht; // 传到hash中是key, key模型
};

我们的上层使用的是unordered_map的话,我们就是key_value模型,那么我们传入hash中应该是key, 以及key_value的键值对模型,如下:

template<class K, class V>
class Unordered_Map
{
public:private:HashT::HashTable<K, pair<K, V>> _ht; // 传到hash中是pair<K, V>模型的键值对
};

我们来一个关系图表现一下关系结构:

T的类型是啥,取决于上层传来的是啥类型,上层传来key类型,那么就是T接收的是key,上层传来的是pair<key, value>键值对的话,T接收的就是pair类型。哈希结点的模板参数也应该由原来的K、V变为T,因为结点只接收一个类型,key就是键值,key,value就是键值对。
在这里插入图片描述

我们有了上面的的例子,我们的哈希桶结点也需要重新定义一下:

	template<class T>struct HashNode{HashNode(const T data):_kv(kv), _next(nullptr){}T _data;HashNode<T>* _next;};

2、仿函数

为什么有个仿函数在这里呢?因为我们的map是键值对,也就是pair<k,v>结构的,我们主要要拿到的是键值key,而我们的编译器/容器底层根本分不清哪个是键值对哪个是键值,也并不知道哈希节点存的到底是个啥类型,所以我们要向上层提供一个仿函数来告诉上层我们所要取到的是键值key,所以此时引入一个仿函数如下:

template<class K, class V>
class Unordered_Map
{struct KeyOfMap // 仿函数,取key{const K& operator()(const pair<K, V>& kv){return kv.first;  // 取key}};
public:private:HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对
};

同样了,set也需要引入仿函数,也就是一个陪跑作用,因为它本身存储的是键值,没存储键值对,但因为编译器分不清,所以还是跟着陪跑,写一下仿函数。还是一个很好理解的例子,女朋友map去逛街,作为男朋友set必须得去陪着,不然挨一顿骂。

template<class K>
class Unordered_Set
{struct KeyOfSet // 仿函数{const K& operator()(const K& key){return key; // 陪跑功能}};
public:private:HashT::HashTable<K, K> _ht; // 传到hash中是key, key模型
};

我们加一个仿函数用这个仿函数模板来接收。
在这里插入图片描述

3、string类型无法取模

字符串无法取模,是哈希问题中最常见的问题,string类的不能取模,无法比较该咋办呢?

因为是整型能够通过仿函数直接拿到,整型类的仿函数我们能够直接写出来并拿到,但是我们日常生活中用string类的还是挺多的,就比如我们的字典需要用英文的字母组词,所以我们用各个英文名做键值,而字符串并不是整型,也就意味着字符串不能直接用于计算哈希地址,我们需要通过某种方法将字符串转换成整型后,才能代入哈希函数计算哈希地址。而有人就提出来了,我们按照字母进行转化成数字就好了,可是计算机中存储整型毕竟是有限的,而字母组成是无限的,这就导致了我们无论用什么方法进行转化成整型,其哈希冲突是不可避免的,也就是哈希桶下必然会挂结点,而我们需要考虑的是怎么样让哈希冲突更小,概率更低。

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

那我们就加一个仿函数吧!一个用来控制将string转化成整型,另一个就用来默认的整形,也就是其本身传入的就是整型,那么就不需要过多地进行转换,直接是整型即可。

	// 将kv.first强转成size_t// 仿函数template<class K>struct DefaultHashTable{size_t operator()(const K& key){return (size_t)key;}};// 模版的特化(其他类型) template<>struct DefaultHashTable<string>{size_t operator()(const string& str){// BKDRsize_t hash = 0;for (auto ch : str){// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等hash *= 131;hash += ch;}return hash;}};

三、哈希表成员函数

1、构造函数

构造函数中有两个成员变量,一个是哈希表,一个是数量,我们将其进行定义:
第一个是vector类型的,直接能进行默认拷贝构造,下一个给一个缺省的哈希表中的有效元素个数为0。

在这里插入图片描述

但因为后续我们要进行拷贝构造,编写了拷贝构造函数后,默认的构造函数就不会生成了,此时我们需要使用default关键字显示指定生成默认构造函数。

在这里插入图片描述

在这里插入图片描述

2、拷贝构造函数

这个拷贝需要深拷贝,因为要一个个点挂过去,浅拷贝拷贝出来的哈希表和原哈希表中存储的都是同一批结点。

拷贝构造的逻辑如下:

  1. 将哈希表调整到ht._table.size()一样的大小。
  2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中
  3. 更改哈希表中有效的数据的量
		// 拷贝构造HashTable(const HashTable& ht){//1. 将哈希表调整到ht._table.size()一样的大小。//2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中//3. 更改哈希表中有效的数据的量_table.resize(ht._table.size());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] = cur;cur = cur->_next;}}}_n = ht._n;}

3、赋值运算符重载

直接交换即可,因为是我们依据参数进行间接调用拷贝构造,之后将拷贝构造出来的哈希表和当前哈希表的两个成员变量分别进行交换即可。

		// 运算符重载HashTable& operator=(HashTable ht){// 通过间接调用拷贝构造进行运算符赋值重载_table.swap(ht._table);swap(_n, ht._n);return *this;}

4、析构函数

因为之前我们的哈希表中的结点是new出来的,所以我们就简单地遍历将每一个结点都delete即可。

		// 析构函数~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; // 哈希桶置空}}}

5、哈希桶的正向迭代器

因为是哈希的正向迭代器实际上是封装了哈希结点指针,但由于++等的操作,可能需要去找哈希桶中下一个不为空的位置,所以每一个迭代器中都含有存储哈希表的地址pht!

搭个框架

	// 哈希正向迭代器template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>struct HTIterator{typedef HashNode<T> Node; // 哈希结点类型typedef HashTable<K, T, KeyOfT, HashFunc> HT; // 哈希表类型typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self; // 迭代器类型typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;  // 迭代器,用来传参的// 成员变量Node* _node;const HT* _pht;};

构造函数

因为是既有结点也有哈希桶的地址,所以两个都需要进行初始化。

		// 构造函数HTIterator(Node* node, const HT* pht):_node(node),_pht(pht){}

正向迭代器解引用操作

返回结点数据的引用即可。

		// RefRef operator*(){return _node->_data; // 返回引用}

正向迭代器指针指向操作

返回结点数据的地址即可。

		// PtrPtr operator->(){return &_node->_data; // 返回地址}

判断两个正向迭代器是否相等

直接判断地址即可,因为是只需要看这两个迭代器封装的结点的类型即可。

		// 判断相等bool operator!=(const Self& s) const{return _node != s._node;}// 判断不相等bool operator==(const Self& s) const{return _node == s._node;}

operator++运算符

哈希桶++的逻辑那可比树能简单不少,逻辑很简单:

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

来个图好理解:
在这里插入图片描述

		Self& operator++(){//1. 若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点//2. 若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点if (_node->_next) // 桶后面的结点还有{_node = _node->_next; // ++操作就是这个桶的当前结点的下一个结点}else // 正好到这个桶的最后一个结点{HashFunc hf;KeyOfT kot;size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();hashi++;while (hashi < _pht->_table.size()) // 表没走完{if (_pht->_table[hashi]) // 表不为空{_node = _pht->_table[hashi]; // 刚刚好就是哈希表下一个的头结点return *this;}++hashi; // 为空则++}_node = nullptr;}return *this;}

6、const迭代器

		// constHTIterator(const Iterator& it):_node(it._node), _pht(it._pht){}

7、哈希表中的操作

  1. 进行正向和const迭代器类型的typedef,需要注意的是,为了让外部能够使用typedef后的正向迭代器类型iterator,我们需要在public区域进行typedef。
  2. 由于正向迭代器中++运算符重载函数在寻找下一个结点时,会访问哈希表中的成员变量_table,而_table成员变量是哈希表的私有成员,因此我们需要将正向迭代器类声明为哈希表类的友元。

在这里插入图片描述

(1)begin() 和 end()

begin():返回哈希桶中第一个有值结点的第一个位置的迭代器。
end():返回哈希桶最后一个结点的值的后一个元素,也就是空。

		// 正向迭代器的begin()iterator begin(){// 找第一个有值的头结点桶for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];if (cur){return iterator(cur, this);}}return iterator(nullptr, this);}// 正向迭代器的end()iterator end(){// 是最后一个结点的下一个位置也就是空位置return iterator(nullptr, this);}// const迭代器的begin()const_iterator begin() const{// 找第一个有值的头结点桶for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];if (cur){return const_iterator(cur, this);}}return const_iterator(nullptr, this);}// const迭代器的end()const_iterator end() const{return const_iterator(nullptr, this);}

(2)GetNextPrime

我们在上一篇博客讲过,素数因为它的因子少,所以更加适合当做容量来进行操作(因为哈希桶挂的少,效率高),所以我们总结以2倍的素数进行排列,每次增容的时候取表中的数据即可:

		size_t GetNextPrime(size_t prime){static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] ={53,         97,         193,       389,       769,1543,       3079,       6151,      12289,     24593,49157,      98317,      196613,    393241,    786433,1572869,    3145739,    6291469,   12582917,  25165843,50331653,   100663319,  201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};size_t i = 0;for (; i < PRIMECOUNT; ++i){if (primeList[i] > prime)return primeList[i];}return primeList[i];}// 构造函数HashTable(){_table.resize(GetNextPrime(1), nullptr);}

(3)Find函数修改

将哈希表中查找函数返回的结点指针,改为返回由结点指针和哈希表地址构成的正向迭代器

在这里插入图片描述

(4)Insert函数修改

		// 插入pair<iterator, bool> Insert(const T& data){// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整// 3将键值对插入哈希表// 4哈希表中的有效元素个数加一KeyOfT kot;HashFunc hf;// 1、查看键值对,调用Find函数iterator key_ = Find(kot(data));if (key_ != end()){return make_pair(key_, false);}// 2、判断负载因子,负载因子是1的时候进行增容if (_n == _table.size()) // 整个哈希表都已经满了{// 增容// a.创建一个新表,将原本容量扩展到原始表的两倍size_t HashiNewSize = GetNextPrime(_table.size());vector<Node*> NewHashiTable;NewHashiTable.resize(HashiNewSize, nullptr);// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中for (size_t i = 0; i < _table.size(); ++i){if (_table[i]) // 这个桶中的有数据/链表存在{Node* cur = _table[i]; // 记录头结点while (cur){Node* next = cur->_next; // 记录下一个结点size_t hashi = hf(kot(data)) % _table.size(); // 记录一下新表的位置// 头插到新表中cur->_next = _table[hashi];_table[hashi] = cur;cur = next; // 哈希这个桶的下一个结点}_table[i] = nullptr;}}// c.交换两个表_table.swap(NewHashiTable);}// 3、将键值对插入到哈希表中size_t hashii = hf(kot(data)) % _table.size();Node* newnode = new Node(data);// 头插法newnode->_next = _table[hashii];_table[hashii] = newnode;// 4、将_n++++_n;return make_pair(iterator(newnode, this), true);}

(5)Erase函数修改

// 删除bool Erase(const K& key){//1通过哈希函数计算出对应的哈希桶编号//2遍历对应的哈希桶,寻找待删除结点//3若找到了待删除结点,则将该结点从单链表中移除并释放//4删除结点后,将哈希表中的有效元素个数减一KeyOfT kot;HashFunc hf;size_t hashi = hf(key) % _table.size();// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点// cur记录的是当前结点,当然是要删除的结点Node* prev = nullptr;Node* cur = _table[hashi];while (cur) // 遍历到结尾{if (kot(cur->_data) == key) // 刚好找到这个值{// 第一种情况:这个要删除的值刚好是哈希桶的头结点if (prev == nullptr){_table[hashi] = cur->_next;}// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值else // (prev != nullptr){prev->_next = cur->_next;}delete cur; // 删除cur结点_n--;return true;}prev = cur;cur = cur->_next;}return false;}

四、Unordered_set和Unordered_map封装

namespace JRH
{template<class K>class Unordered_Set{struct KeyOfSet // 仿函数{const K& operator()(const K& key){return key; // 陪跑功能}};public:typedef typename HashT::HashTable<K, K, KeyOfSet>::iterator iterator;typedef typename HashT::HashTable<K, K, KeyOfSet>::const_iterator 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);}// 删除void erase(const K& key){_ht.Erase(key);}private:HashT::HashTable<K, K, KeyOfSet> _ht; // 传到hash中是key, key模型};
}
namespace JRH1
{template<class K, class V>class Unordered_Map{struct KeyOfMap // 仿函数,取key{const K& operator()(const pair<K, V>& kv){return kv.first;  // 取key}};public://现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::iterator iterator;typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::const_iterator 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 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;}// 查找iterator find(const K& key){return _ht.Find(key);}// 删除void erase(const K& key){_ht.Erase(key);}private:HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对};
}

五、代码汇总

HashTable.h

#include<vector>
#include<string>
using namespace std;namespace HashT
{// 将kv.first强转成size_t// 仿函数template<class K>struct DefaultHashTable{size_t operator()(const K& key){return (size_t)key;}};// 模版的特化(其他类型) template<>struct DefaultHashTable<string>{size_t operator()(const string& str){// BKDRsize_t hash = 0;for (auto ch : str){// 131是一个素数,也可以用其他一些素数代替,主要作用防止两个不同字符串计算得到的哈希值相等hash *= 131;hash += ch;}return hash;}};template<class T>struct HashNode{HashNode(const T data):_data(data), _next(nullptr){}T _data;HashNode<T>* _next;};// 前置声明template<class K, class T, class KeyOfT, class HashFunc>class HashTable;// 哈希正向迭代器template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>struct HTIterator{typedef HashNode<T> Node; // 哈希结点类型typedef HashTable<K, T, KeyOfT, HashFunc> HT; // 哈希表类型typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self; // 迭代器类型typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;  // 迭代器,用来传参的// 成员变量Node* _node;const HT* _pht;// 构造函数HTIterator(Node* node, const HT* pht):_node(node),_pht(pht){}// constHTIterator(const Iterator& it):_node(it._node), _pht(it._pht){}// PtrPtr operator->(){return &_node->_data; // 返回地址}// RefRef 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++(){//1. 若当前结点不是当前哈希桶中的最后一个结点,则++后走到当前哈希桶的下一个结点//2. 若当前结点是当前哈希桶的最后一个结点,则++后走到下一个非空哈希桶的第一个结点if (_node->_next) // 桶后面的结点还有{_node = _node->_next; // ++操作就是这个桶的当前结点的下一个结点}else // 正好到这个桶的最后一个结点{HashFunc hf;KeyOfT kot;size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();hashi++;while (hashi < _pht->_table.size()) // 表没走完{if (_pht->_table[hashi]) // 表不为空{_node = _pht->_table[hashi]; // 刚刚好就是哈希表下一个的头结点return *this;}++hashi; // 为空则++}_node = nullptr;}return *this;}};//哈希表template<class K, class T, class KeyOfT, class HashFunc = DefaultHashTable<K>>class HashTable{typedef HashNode<T> Node;// 友元声明template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;public:typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;typedef HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;// 正向迭代器的begin()iterator begin(){// 找第一个有值的头结点桶for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];if (cur){return iterator(cur, this);}}return iterator(nullptr, this);}// 正向迭代器的end()iterator end(){// 是最后一个结点的下一个位置也就是空位置return iterator(nullptr, this);}// const迭代器的begin()const_iterator begin() const{// 找第一个有值的头结点桶for (size_t i = 0; i < _table.size(); ++i){Node* cur = _table[i];if (cur){return const_iterator(cur, this);}}return const_iterator(nullptr, this);}// const迭代器的end()const_iterator end() const{return const_iterator(nullptr, this);}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];}// 构造函数HashTable(){_table.resize(GetNextPrime(1), nullptr);}//构造函数//HashTable() = default; //显示指定生成默认构造函数// 拷贝构造HashTable(const HashTable& ht){//1. 将哈希表调整到ht._table.size()一样的大小。//2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中//3. 更改哈希表中有效的数据的量_table.resize(ht._table.size());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] = cur;cur = cur->_next;}}}_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;}}}// 插入pair<iterator, bool> Insert(const T& data){// 1查看哈希表中是否存在该键值的键值对,若已存在则插入失败// 2判断是否需要调整负载因子,若负载因子过大都需要对哈希表的大小进行调整// 3将键值对插入哈希表// 4哈希表中的有效元素个数加一KeyOfT kot;HashFunc hf;// 1、查看键值对,调用Find函数iterator key_ = Find(kot(data));if (key_ != end()){return make_pair(key_, false);}// 2、判断负载因子,负载因子是1的时候进行增容if (_n == _table.size()) // 整个哈希表都已经满了{// 增容// a.创建一个新表,将原本容量扩展到原始表的两倍size_t HashiNewSize = GetNextPrime(_table.size());vector<Node*> NewHashiTable;NewHashiTable.resize(HashiNewSize, nullptr);// b.遍历旧表,顺手牵羊,将原始表数据逐个头插到新表中for (size_t i = 0; i < _table.size(); ++i){if (_table[i]) // 这个桶中的有数据/链表存在{Node* cur = _table[i]; // 记录头结点while (cur){Node* next = cur->_next; // 记录下一个结点size_t hashi = hf(kot(data)) % _table.size(); // 记录一下新表的位置// 头插到新表中cur->_next = _table[hashi];_table[hashi] = cur;cur = next; // 哈希这个桶的下一个结点}_table[i] = nullptr;}}// c.交换两个表_table.swap(NewHashiTable);}// 3、将键值对插入到哈希表中size_t hashii = hf(kot(data)) % _table.size();Node* newnode = new Node(data);// 头插法newnode->_next = _table[hashii];_table[hashii] = newnode;// 4、将_n++++_n;return make_pair(iterator(newnode, this), true);}// 查找iterator Find(const K& key){//1通过哈希函数计算出对应的哈希地址//2通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可HashFunc hf;KeyOfT kot;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi]; // 刚好到哈希桶的位置while (cur){// 找到匹配的了if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}return end();}// 删除bool Erase(const K& key){//1通过哈希函数计算出对应的哈希桶编号//2遍历对应的哈希桶,寻找待删除结点//3若找到了待删除结点,则将该结点从单链表中移除并释放//4删除结点后,将哈希表中的有效元素个数减一KeyOfT kot;HashFunc hf;size_t hashi = hf(key) % _table.size();// prev用来记录前面一个结点,有可能是删除的是哈希桶第一个结点// cur记录的是当前结点,当然是要删除的结点Node* prev = nullptr;Node* cur = _table[hashi];while (cur) // 遍历到结尾{if (kot(cur->_data) == key) // 刚好找到这个值{// 第一种情况:这个要删除的值刚好是哈希桶的头结点if (prev == nullptr){_table[hashi] = cur->_next;}// 第二种情况:这个要删除的值不是哈希桶的头结点,而是下面挂的值else // (prev != nullptr){prev->_next = cur->_next;}delete cur; // 删除cur结点_n--;return true;}prev = cur;cur = cur->_next;}return false;}public:// 打印一下void Print(){// 大循环套小循环for (size_t i = 0; i < _table.size(); ++i){printf("[%d]->", i);Node* cur = _table[i];// 小循环while (cur){//cout << cur->_data;cur = cur->_next;}printf("NULL\n");}}private:vector<Node*> _table; //哈希表size_t _n = 0; //哈希表中的有效元素个数};
}

Unordered_map.h

#include"HashTable.h"namespace JRH1
{template<class K, class V>class Unordered_Map{struct KeyOfMap // 仿函数,取key{const K& operator()(const pair<K, V>& kv){return kv.first;  // 取key}};public://现在没有实例化,没办法到HashTable里面找iterator,所以typename就是告诉编译器这里是一个类型,实例化以后再去取typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::iterator iterator;typedef typename HashT::HashTable<K, pair<K, V>, KeyOfMap>::const_iterator 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 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;}// 查找iterator find(const K& key){return _ht.Find(key);}// 删除void erase(const K& key){_ht.Erase(key);}private:HashT::HashTable<K, pair<K, V>, KeyOfMap> _ht; // 传到hash中是pair<K, V>模型的键值对};
}

Unordered_set.h

#include"HashTable.h"namespace JRH
{template<class K>class Unordered_Set{struct KeyOfSet // 仿函数{const K& operator()(const K& key){return key; // 陪跑功能}};public:typedef typename HashT::HashTable<K, K, KeyOfSet>::iterator iterator;typedef typename HashT::HashTable<K, K, KeyOfSet>::const_iterator 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);}// 删除void erase(const K& key){_ht.Erase(key);}private:HashT::HashTable<K, K, KeyOfSet> _ht; // 传到hash中是key, key模型};
}

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

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

相关文章

pytorch_神经网络构建1

文章目录 pytorch简介神经网络基础分类问题分析:逻辑回归模型逻辑回归实现多层神经网络多层网络搭建保存模型 pytorch简介 为什么神经网络要自定义数据类型torch.tensor? tensor可以放在gpu上训练,支持自动求导,方便快速训练,同时支持numpy的运算,是加强版,numpy不支持这些 为…

python二次开发CATIA:导出Excel格式BOM

BOM是英文Bill of Material的缩写&#xff0c;中文翻译为“物料清单”&#xff0c;也称为产品结构表或产品结构树。它是计算机可以识别的产品结构数据文件&#xff0c;也是ERP的主导文件。BOM使系统识别产品结构&#xff0c;也是联系与沟通企业各项业务的纽带。ERP系统中的BOM的…

多线程经典代码案例及手动实现

目录 一.线程和多线程 二. 多线程的经典的代码案例 1.单例模式 2.阻塞队列 (1)概念介绍 (2)生产者消费者模型 (3)手动实现阻塞队列 (4)代码解释及问题分析 3.定时器 (1)概念介绍 (2)思路分析 (3)手动实现定时器 (4)代码解释及问题分析 问题一:优先级 问题二 :忙等…

基于SSM的电动车上牌管理系统(有报告)。Javaee项目。

演示视频&#xff1a; 基于SSM的电动车上牌管理系统&#xff08;有报告&#xff09;。Javaee项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringM…

Java基本数据类型和变量

目录 一、基本数据类型 1.1 整型 1.1.1 byte 1.1.2 short 1.1.3 int 1.1.4 long 1.2 浮点型 1.2.1 float 1.2.2 double 1.3 字符型 1.4 布尔型 二、变量 2.1 变量的概念 2.2 语法格式 2.3 整型变量 2.3.1 整型变量 2.3.2 长整型变量 2.3.3 短整型变量 2.3.…

MySQL之DQL

DQL是数据查询语言 SELECT语句 语法&#xff1a; SELECT {*,列名&#xff0c;函数等} FROM 表名;SELECT *&#xff1a;表示匹配所有列 FROM :提供数据源 例如:查询student表的所有记录 SELECT * FROM student;例如&#xff1a;查询学生姓名和地址&#xff1a; SELECT Stud…

学信息系统项目管理师第4版系列16_资源管理过程

1. 组建项目团队&#xff0c;建设项目团队和管理项目团队属于执行过程组 1.1. 【高22上选21】 1.1.1. 【高21上选25】 1.2. 3版 2. 【高19上案三】 2.1. 【高18上案三】 2.2. 【高23上案一】 3. 规划资源管理 3.1. 定义如何估算、获取、管理和利用团队以及实物资源的过…

mstsc无法保存RDP凭据, 100%生效

问题 即使如下两项都打勾&#xff0c;其还是无法保存凭据&#xff0c;特别是连接Ubuntu (freerdp server)&#xff1a; 解决方法 网上多种复杂方法&#xff0c;不生效&#xff0c;其思路是修改后台配置&#xff0c;以使mstsc跟平常一样自动记住凭据。最后&#xff0c;如下的…

斯坦福数据挖掘教程·第三版》读书笔记(英文版)Chapter 10 Mining Social-Network Graphs

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT。 Chapter 10 Mining Social-Network Graphs The essential characteristics of a social network are: There is a collection of entities that participate in the network. Typically, these entiti…

Python学习笔记之分支结构与循环结构

Python学习笔记之分支结构与循环结构 一、分支结构 使用关键字if、elif、else 练习1&#xff1a;使用分支结构实现分段函数求值 """分段函数求值""" x float(input("x "))if x > 1:y 3 * x - 5 elif x < -1:y 5 * x 3…

2023/10/4 -- ARM

今日任务&#xff1a;QT实现TCP服务器客户端搭建的代码&#xff0c;现象 ser&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);server new QTcpSe…

免费、丰富、便捷的资源论坛——Yiove论坛,包括但不限于阿里云盘、夸克云盘、迅雷云盘等等

引言 目前资源的数量达到了60000&#xff0c;六万多的资源意味着在这里几乎可以找到任何你想要的资源。 当然&#xff0c;资源并不是论坛的全部&#xff0c;其中还包括了技术交流、福利分享、最新资讯等等。 传送门&#xff1a;YiOVE论坛 - 一个有资源有交流&#xff0c;有一…

PCL 计算点云中值

目录 一、算法原理2、主要函数二、代码实现三、结果展示四、参考链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 计算点云坐标的中值点,首先对点云坐标进行排序,然后计算中值。如果点云点的个数为奇数…

计组—— I/O系统

&#x1f4d5;&#xff1a;参考王道课件 目录 一、I/O系统的基本概念 1.什么是“I/O”&#xff1f; ​编辑2.主机如何和I/O设备进行交互&#xff1f; 3.I/O控制方式 &#xff08;1&#xff09;程序查询方式 &#xff08;2&#xff09;程序中断方式 &#xff08;3&#x…

号卡推广管理系统源码/手机流量卡推广网站源码/PHP源码+带后台版本+分销系统

源码简介&#xff1a; 号卡推广管理系统源码/手机流量卡推广网站源码&#xff0c;基于PHP源码&#xff0c;而且它是带后台版本&#xff0c;分销系统。运用全新UI流量卡官网系统源码有后台带文章。 这个流量卡销售网站源码&#xff0c;PHP流量卡分销系统&#xff0c;它可以支持…

C#餐饮收银系统

一、引言 餐饮收银系统是一种用于管理餐馆、咖啡厅、快餐店等餐饮业务的计算机化工具。它旨在简化点餐、结账、库存管理等任务&#xff0c;提高运营效率&#xff0c;增强客户体验&#xff0c;同时提供准确的财务记录。C# 餐饮收银系统是一种使用C#编程语言开发的餐饮业务管理软…

【Java】微服务——Ribbon负载均衡(跟进源码分析原理)

添加LoadBalanced注解&#xff0c;即可实现负载均衡功能&#xff0c;这是什么原理 1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件&#xff0c;来实现负载均衡功能的。 2.源码跟踪 为什么我们只输入了service名称就可以访问了呢&#xff1f;之前还要获取…

MySQL - mysql服务基本操作以及基本SQL语句与函数

文章目录 操作mysql客户端与 mysql 服务之间的小九九了解 mysql 基本 SQL 语句语法书写规范SQL分类DDL库表查增 mysql数据类型数值类型字符类型日期类型 示例修改&#xff08;表操作&#xff09; DML添加数据删除数据修改数据 DQL查询多个字段条件查询聚合函数分组查询排序查询…

conda安装使用jupyterlab注意事项

文章目录 一、conda安装1.1 conda安装1.2 常见命令1.3 常见问题 二、jupyterlab2.1 jupyterlab安装和卸载2.2 常见错误2.2.1 版本冲突&#xff0c;jupyterlab无法启动2.2.2 插件版本冲突 2.3 常用插件2.3.1 debugger2.3.2 jupyterlab_code_formatter 2.4 jupyter技巧 一、conda…

iOS---生成证书文件的时候无法选择导出.p12文件

解决办法&#xff1a; 左栏有两个分类&#xff0c;一个钥匙串&#xff0c;一个是种类&#xff0c;要选择种类里面的【我的证书】或【证书】进行导出。选择【系统】找到【我的证书】这样导出不了"个人信息交换(.p12)" 正确做法是&#xff1a;选择【登录】找到【我的…