【数据结构】【C++】封装哈希表模拟实现unordered_map和unordered_set容器

【数据结构】&&【C++】封装哈希表模拟实现unordered_map和unordered_set容器

  • 一.哈希表的完成
  • 二.改造哈希表(泛型适配)
  • 三.封装unordered_map和unordered_set的接口
  • 四.实现哈希表迭代器(泛型适配)
  • 五.封装unordered_map和unordered_set的迭代器
  • 六.解决key不能修改问题
  • 七.实现map[]运算符重载

一.哈希表的完成

在上一篇哈希表的模拟实现中,已经将哈希表实现完毕,这里不再分析。

#pragma once
using namespace std;
#include <vector>
#include<iostream>//哈希桶//1.第一哈希表的完成//哈希结点
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>
struct defaulthashfunc//默认的仿函数可以让数据模
{size_t operator()(const K& key){return (size_t)key;//将key强行类型转化//这样作的意义:可以负数也可以进行模了}
};
template <>
//模板的特化,当数据类型是int的时候就默认使用defaulthashfunc<int>,当数据类型是string类型时,就默认使用defaulthashfunc<string>
struct defaulthashfunc<string>
{size_t operator()(const string& str){//为了减少冲突,我们将字符串的每个字符的值相加size_t hash = 0;for (auto& it : str){hash *= 131;hash += it;}return hash;}
};//要写一个仿函数?  因为不是所有的数据类型都可以模的
// 一般整数是可以模的,string类型是无法模的
// 所以我们要写一个仿函数来达到传的数据可以模
//这样也就是增加了哈希表的一个模板参数l
template<class K,class V,class Hashfunc=defaulthashfunc<K>>
//哈希表
class Hash_table
{typedef HashNode<K, V> Node;//哈希需要将写析构函数,虽然自定义类型vector会自动调用默认析构,但它里面的成员是内置类型,没有默认构造,//所以需要我们自己析构每个结点
public:~Hash_table(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;//要先保存起来delete cur;cur = next;}_table[i] = nullptr;}}Hash_table(){//构造_table.resize(10, nullptr);//首先开出十个空间,每个空间值为空}bool insert(const pair<K, V> _kv){Hashfunc hf;//仿函数可以使数据模//在插入之前,确认一下表里是否已经有了这个值,如果有了就不用插入了if (find(_kv.first)){return false;}//我们自己控制扩容逻辑,虽然vector会自己扩容,但我们要控制。因为扩容完,有的key会冲突有的值又不冲突了。//如果不扩容,那么冲突多了就根单链表一样了//当负载因子大约等于1时就要扩容,平均每个桶里有一个数据if (n == _table.size()){//异地扩容,重新开空间size_t newSize = _table.size() * 2;vector<Node*> newtable;newtable.resize(newSize, nullptr);//不能再复用下面的方法,这样不好,因为就又重开空间,然后又要释放,//我们应该将原来的结点拿过来使用//所以我们遍历旧表,将旧表的结点拿过来,签到新表上for (size_t i = 0; i < _table.size(); i++){//扩容后,空间size变大了,有的数据就可能会存到不同的桶里了//拿下来的结点要重新计算放进哪个位置Node* cur = _table[i];//cur后面可能还有链接的结点while (cur){size_t hashi = hf(cur->_kv.first) % newtable.size();Node* next = cur->_next;//头插到新表cur->_next = newtable[hashi];//头插 这个结点的 接到插入结点的前面对吧//那么next就接到newtavle[i]newtable[hashi] = cur;//往后走接着拿走cur = next;}//当前桶里的结点被拿光后,就置为空_table[i] = nullptr;}//这个新表就是我们想要的表,那么我们利用vector、的交换,让旧表和新表交换_table.swap(newtable);}size_t hashi = hf(_kv.first) % _table.size();Node* newnode = new Node(_kv);newnode->_next = _table[hashi];_table[hashi] = newnode;//将新结点头插到哈希桶里++n;return true;}Node* find(const K& key){Hashfunc hf;size_t hashi = hf(key)% _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key)return cur;elsecur = cur->_next;}return nullptr;}void Print(){for (int i = 0; i < _table.size(); i++){printf("[%d]", i);Node* cur = _table[i];while (cur){cout << cur->_kv.first << " ";cur = cur->_next;}cout << endl;}}bool erase(const K& key){Hashfunc hf;//可以复用find吗?先用find找到key然后删除key呢?4//不可以,因为删除一个结点需要找到这个结点的前面和后面的位置,但这里只有key的位置,所以不能直接复用find,但是复用其中的步骤size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key)//找到要删除的结点后{//将前面的结点的指针指向后面的前面//还有一种可能cur就是桶里的第一个,那么就是头删了,prev就是nullptrif (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;//每次先记录一下前面的结点位置cur = cur->_next;}}return false;}
private://底层封装的是一个数组//数组里的指针指向的都是哈希结点vector<Node*> _table;//底层还封装一个大小n表示实际表中的数据个数size_t n=0;///用来计算负载因子
};

二.改造哈希表(泛型适配)

我们要对哈希表进行改造,因为unordered_map和unordered_set底层用的都是哈希表,虽然不是同一个哈希表,但是是同一个模板实例化出来的哈希表。我们要让哈希表可以适配存储不同的数据类型,因为unordered_set里面存的是K类型,而unordered_map里面存的是pair<K,V>类型。
所以我们一开始并不知道哈希表里存的数据是什么类型,那么就用T表示。当传的模板参数是K类型,哈希表就实例化存的就是K类型,当传的模板参数是pair<K,V>类型,哈希表实例化存的就是pair<K,V>类型,所以我们可以通过传不同的模板参数来决定哈希表里存的是什么数据类型。
所以我们需要改造哈希表,将里面的数据都改成T类型,修改成T类型的原因是我们不知道是什么数据类型,根据传的模板参数决定。


template <class T>
//定义哈希结点
struct HashNode
{T _data;//存储的数据是T类型,根据传过来的模板参数确定HashNode<T>* _next;//指向下一个结点的指针HashNode(const T& data):_data(data), _next(nullptr){}
};
// [问题]:一旦泛型后,我们就不知道数据的具体类型了,那么我们要利用除留余数法计算哈希地址时(我们都是利用key来进行取模的而我们不知道具体的类型是K类型还是pair<K,V>类型),就要写一个仿函数,这个仿函数根据map和set传过来的,然后实例化出哈希表中的仿函数。获取数据中的key,让key进行计算```c
template<class K, class T>
//哈希表
class Hash_table
{typedef HashNode<T> Node;public:~Hash_table(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;//要先保存起来delete cur;cur = next;}_table[i] = nullptr;}}Hash_table(){//构造_table.resize(10, nullptr);//首先开出十个空间,每个空间值为空}bool insert(const T& data){…………………}bool find(const K& key){………………}bool erase(const K& key){………………	}
private:vector<Node*> _table;//底层还封装一个大小n表示实际表中的数据个数size_t n = 0;//用来计算负载因子
};

存在问题:
这里不管是插入还是查找还是删除第一步都是需要将数据的哈希地址找到,而哈希地址是利用除留余数法计算得到的,是利用key值进行取模的,但这里一旦适配泛型后,我们就不知道具体的类型是K类型还是pair<K,V>类型,如果是K类型那么就可以直接取模,如果是pair<K,V>类型,那是不可以直接取模的,需要将里面的key值取出来。
解决方法:
我们可以利用一个仿函数,这个仿函数的功能是可以将数据里的Key类型数据取出来。那么我们可以给哈希表增加一个模板参数,给仿函数用。一旦遇到要计算哈希地址或者比较的操作时,我们就可以将数据里的K值取出来进行计算比较。
仿函数实现的原理:当T类型是K类型数据时,直接返回K值即可,当T类型是pair<K,V>数据时,返回里面的first数据即可(就是K值)。

template<class K, class T, class KeyOfT,class Hashfunc = defaulthashfunc<K>>
//哈希表
class Hash_table
{typedef HashNode<T> Node;
public:Hash_table(){//构造_table.resize(10, nullptr);//首先开出十个空间,每个空间值为空}bool insert(const T& data){KeyOfT kt;Hashfunc hf;//仿函数可以使数据模//在插入之前,确认一下表里是否已经有了这个值,如果有了就不用插入了if (find(kt(data))){return false;}if (n == _table.size()){//异地扩容,重新开空间size_t newSize = _table.size() * 2;vector<Node*> newtable;newtable.resize(newSize, nullptr);for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];//cur后面可能还有链接的结点while (cur){size_t hashi = hf(kt(cur->_data)) % newtable.size();Node* next = cur->_next;cur->_next = newtable[hashi];newtable[hashi] = cur;//往后走接着拿走cur = next;}//当前桶里的结点被拿光后,就置为空_table[i] = nullptr;}//这个新表就是我们想要的表,那么我们利用vector、的交换,让旧表和新表交换_table.swap(newtable);}size_t hashi = hf(kt(data)) % _table.size();//这里有两个仿函数kt是用来获取data数据里的key值//hf是用来适配不同的K值,因为string类型无法取模Node* newnode = new Node(data);newnode->_next = _table[hashi];_table[hashi] = newnode;//将新结点头插到哈希桶里++n;return true;}iterator find(const K& key){Hashfunc hf;KeyOfT kt;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kt(cur->_data) == key)return iterator(cur,this);elsecur = cur->_next;}return iterator(nullptr,this);}bool erase(const K& key){Hashfunc hf;KeyOfT kt;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){//这里仿函数kt是用来获取data数据里的key值if (kt(cur->_data) == key)//找到要删除的结点后{//将前面的结点的指针指向后面的前面//还有一种可能cur就是桶里的第一个,那么就是头删了,prev就是nullptrif (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--n;return true;}else{prev = cur;//每次先记录一下前面的结点位置cur = cur->_next;}}return false;}
private:vector<Node*> _table;//底层还封装一个大小n表示实际表中的数据个数size_t n = 0;//用来计算负载因子
};

三.封装unordered_map和unordered_set的接口

封装set,内部实现仿函数,然后底层封装的是存储K值(结点指针)的哈希表。

#include"Hash.h"
namespace tao
{template<class K>class set{struct setoft//仿函数用来获取数据的key值{const K& operator()(const K& key){return key;}};public:bool insert(const K& key){return _ht.insert(key);}private://底层封装一个哈希表,哈希表里存的是K类型Hash_table<K, K, setoft> _ht;};
};

封装map,实现仿函数,底层存储的是pair<K,V>类型的(结点指针)哈希表。

#include"Hash.h"
namespace tao
{template<class K, class V>class map{struct mapoft//仿函数获取数据中的key值{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:bool insert(const pair<K, V>& kv){return _ht.insert(kv);}private://底层封装一个哈希表,哈希表里的数据是pair<K,V>类型Hash_table<K, pair<const K, V>, mapoft> _ht;};
};

四.实现哈希表迭代器(泛型适配)

哈希表的迭代器是一个自定义类型,因为原生的结点指针不能满足我们想要的操作,所以我们就直接将结点的指针封装起来,然后自定义一个类型,对这个结点指针增加一些操作来完成我们想要的效果。
首先哈希表的迭代器里肯定要封装一个结点的指针,++后找到下一个结点,在一个哈希桶里,++就代表着找链表下面的结点即可,那么如果该结点就是链表最后一个结点呢?怎么找到下一个不为空的桶呢?又或者你是如何找到第一个不为空的桶的呢?
所以这里我们还需要一个指向哈希表的指针,这样我们才可以找到桶与桶之间的关系,而结点指针是用来找结点与结点之间的关系的。

哈希表的迭代器里封装两个对象,一个是结点指针,一个哈希表指针。

1.迭代器有普通迭代器和const迭代器,普通迭代很好实现,那么const迭代器如何实现呢?
与链表的const迭代器实现原理一样,我们通过三个模板参数(template <class T,class Ref,class Ptr>)来控制函数的返回值,从而控制返回的是普通类型的迭代器还是const类型的迭代器。这里也就是泛型适配,适配多种类型 。

Ref控制解引用函数的返回值,当Ref为T&时,返回的就是普通迭代器,当Ref为const T&时,返回的就是const迭代器。
Ptr控制的->重载函数的返回值,当Ptr为T时,返回的就是普通迭代器,当Ptr为const T时,返回的就是const迭代器。

2.哈希表迭代器的++实现原理:

①假设当前结点在一个桶里,这个桶还没走完,那么直接找下一个结点即可
②如果这个结点是桶里最后一个原生,即桶走完了,那么我们就要找下一个不为空的桶
③如何找到下一个不为空的桶呢?首先将当前结点的哈希地址计算出来,然后将哈希地址++,再利用一个循环,查找后面的桶是否为空,如果不为空,那么这个桶就是最终结果,如果为空就再找后面的桶。

//泛型适配--适配普通迭代器和const迭代器
template<class K, class T,class Ref,class Ptr, class KeyOfT, class Hashfunc>
//哈希表的迭代器里面肯定封装一个结点的指针,但还需要通过表来找到下一个桶,因为我们需要遍历先找到第一个桶,当这个桶遍历完后,怎么找到下一个不为空的桶呢?
//需要通过这个哈希表来找到桶,所以我们还需要一个指向哈希表的指针struct HSIterator
{typedef  HashNode<T> Node;/typedef HSIterator<K, T,Ref,Ptr,KeyOfT, Hashfunc> Self;Node* _node;//底层封装着一个结点指针const Hash_table <K,T, KeyOfT,Hashfunc>* _pht;//底层还封装着一个哈希表指针//这里可以加const因为我们不是根据pht来找到哈希表来修改哈希表里的内容HSIterator(Node* node, Hash_table<K, T, KeyOfT,Hashfunc>* hs):_node(node), _pht(hs){}Ref& operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){if (_node->_next)//当前桶没有走完,那么直接找下一个即可{_node = _node->_next;}else//当前桶走到空了,就要找下一个不为空的桶{KeyOfT kof;Hashfunc hf;size_t hashi = hf(kof(_node->_data)) % _pht->_table.size();//这里我们在外面去调用了哈希表的私有成员,所以我们需要让迭代器成为哈希表的友元//找到当前桶的位置++hashi;//找下一个桶不为空while (hashi<_pht->_table.size()){if (_pht->_table[hashi]){_node = _pht->_table[hashi];return *this;}else{++hashi;}}//走到这里就说明桶走走光了,还没找到_node = nullptr;}return *this;}bool operator!=(const Self& s){return _node != s._node;}};

【存在问题1】:
这里存在一个相互依赖关系问题,因为在哈希表里我们使用了迭代器,在迭代器里我们又使用了哈希表。我们需要在这里使用前置声明告诉编译器,我们是有哈希表,只不过哈希表在后面。这样就不会报错啦。

//这里存在问题,迭代器里要用哈希,哈希里面要有迭代器,相互依赖关系
//我们这里用前置声明告诉编译器,我们要是有哈希表,这个表是存在的,在后面
template<class K, class T, class KeyOfT, class Hashfunc>
class Hash_table;
template<class K, class T,class Ref,class Ptr, class KeyOfT, class Hashfunc>struct HSIterator
{………………………………………………………………………………………………………………………………
};

【存在问题2】:
在迭代器的++里我们在计算当前结点的哈希地址时,取模时,利用哈希指针找到了哈希表里的vector<Node*> _table元素,并访问了它的函数,这里我们在外面调用哈希表的私有成员,这样是不可行,所以我们需要让迭代器成为哈希表的友元类,这样在迭代器里就可以使用哈希表的私有成员了。
在这里插入图片描述

迭代器实现之后,我们就可以在哈希表里,来实现迭代器的begin()和end()了。
begin()就是找哈希表里第一个不为空的桶。
end()就是找最后一个不为空的桶的下一个位置,也就是空。

template<class K, class T, class KeyOfT,class Hashfunc = defaulthashfunc<K>>
//哈希表
class Hash_table
{typedef HashNode<T> Node;template<class K, class T, class Ref,class Ptr,class KeyOfT, class Hashfunc>friend struct HSIterator;//让迭代器成为哈希表的友元
public:typedef HSIterator<K, T,T&,T*, KeyOfT, Hashfunc> iterator;//适配普通迭代器typedef HSIterator<K, T, const T&, const T*, KeyOfT, Hashfunc> const_iterator;//适配const迭代器iterator begin(){//找第一个桶for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){if (cur){return iterator(cur, this);//这里this就是哈希表的指针}}}//最后没有找到return iterator(nullptr, this);}iterator end(){return iterator(nullptr, this);}const_iterator begin()const{//找第一个桶for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){if (cur){return const_iterator(cur, this);}}}//最后没有找到return const_iterator(nullptr, this);}const_iterator end()const{return const_iterator(nullptr, this);}bool insert(const T& data){…………………………}bool erase(const K& key){…………………………}
private:vector<Node*> _table;//底层还封装一个大小n表示实际表中的数据个数size_t n = 0;//用来计算负载因子
};

这样哈希表的迭代器就完成啦。不过这里还有一个问题喔,确实封装哈希表确实比较恶心,比封装红黑树还复杂。
这里的问题在于const修饰begin()和const修饰end()这里是编译不过的。为什么呢?这里提醒没有构造函数可以接收,或者构造函数重载不明确。
在这里插入图片描述
这里的原因是因为const修饰了this指针,导致指向哈希表的指针变成const类型了,而迭代器的构造里,是用普通迭代器构造的。所以当this指针传过去构造时,const是不能传给普通类型的,权限放大了。所以这里我们只需要重载一个参数类型是const类型的哈希表指针即可。

	HSIterator(Node* node, Hash_table<K, T, KeyOfT,Hashfunc>* hs)//hs是普通的类型:_node(node), _pht(hs){}//当传过来的哈希表指针是普通指针就走上面--权限的缩小//重载一个,当传过来的是const修饰的哈希表指针就走这里---权限的平移HSIterator(Node* node, const Hash_table<K, T, KeyOfT, Hashfunc>* hs)//hs是普通的类型:_node(node), _pht(hs){}

五.封装unordered_map和unordered_set的迭代器

只有哈希表里的迭代器完成了,才可以封装map和set里的迭代器。
封装set的迭代器,本质就是调用哈希表的迭代器接口。

1.不过要注意的是,在重命名红黑树里的迭代器时,需要在类名前面加上typename,如果不加上typename是不行的,因为这时类模板还没有实例化出对象出来,就算实例化了,也有部分类型没有实例,因为编译器也不知道这个是内置类型还是静态变量,加上是告诉编译器这个是类型,这个类型在类模板里定义,等类模板实例化后再找。
2.定义好普通迭代和const迭代器后,就可以实现begin()和end()了。

namespace tao
{template<class K>class set{struct setoft{const K& operator()(const K& key){return key;}};public:typedef typename Hash_table<K, K, setoft>::iterator iterator;typedef typename Hash_table<K, K, setoft>::const_iterator const_iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}iterator begin()const{return _ht.begin();}iterator end()const{return _ht.end();}bool insert(const K& key){return _ht.insert(key);}private://底层封装一个哈希表,哈希表里存的是K类型Hash_table<K, K, setoft> _ht;};
};

封装map的迭代器,本质上就是调用哈希表里的迭代器接口。

#include"Hash.h"
namespace tao
{template<class K, class V>class map{struct mapoft{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename Hash_table<K, pair<K, V>, mapoft>::iterator iterator;typedef typename Hash_table<K, pair< K, V>, mapoft>::const_iterator const_iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}iterator begin()const{return _ht.begin();}iterator end()const{return _ht.end();}bool insert(const pair<K, V>& kv){return _ht.insert(kv);}private://底层封装一个哈希表,哈希表里的数据是pair<K,V>类型Hash_table<K, pair< K, V>, mapoft> _ht;};
};

六.解决key不能修改问题

set里面存储的Key值是不能被修改的,map里的存储的K值也是不能被修改,但是Value值是可以被修改!
如果解决这个问题呢?

问题:set里的key值不能被修改。map里的key值不能被修改,value值可以被修改。 set解决原理:
1.set里存储的值就只有Key值,索性我们直接让这个存储的数据无法被修改,只能访问读取,无法修改。即使用const修饰。而我们是通过迭代器来访问到这个数据的,所以我们让普通迭代器变成const迭代器即可。所以在set里,普通迭代器和const迭代器最终都是const迭代器。
2.那么迭代器都是const的了,最终都只会调用const修饰的begin()和end()函数了,普通的begin()和end()就不需要写了。
3.不过这样处理又会出现一个很难搞的问题,这个就是set的insert的返回值问题,我们后面要实现map的[]运算符重载就会讲到。

set的解决方法:public:typedef typename Hash_table<K, K, setoft>::const_iterator iterator;//为了解决set的key不能被修改所以我们让普通迭代器变成const迭代器typedef typename Hash_table<K, K, setoft>::const_iterator const_iterator;//因为普通迭代器也是const所以我们只用写const的begin和end即可。iterator begin()const{return _ht.begin();}iterator end()const{return _ht.end();}//这里的pair<const_iterator,bool>类型的bool insert(const K& key){//return _ht.insert(key);}private://底层封装一个哈希表,哈希表里存的是K类型Hash_table<K, K, setoft> _ht;};
};

map的解决原理
1.在存储的时候就让K值无法修改。
2.因为我们知道map里存储的数据是pair<K,V>类型,我们不能想set那个让普通迭代器变成const迭代器,因为map要求Value的值还是可以修改的,所以不让pair<K,V>类型无法修改,而是单纯的让里面的K值无法修改,也就是在里面用const修饰K,那么这样K值就不能被修改,V值可以被修改。
3.pair是可以修改的,但是里面的K是无法被修改的!

map的解决方法:public:typedef typename Hash_table<K, pair<const K, V>, mapoft>::iterator iterator;typedef typename Hash_table<K, pair<const K, V>, mapoft>::const_iterator const_iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}iterator begin()const{return _ht.begin();}iterator end()const{return _ht.end();}bool insert(const pair<K, V>& kv){return _ht.insert(kv);}private://底层封装一个哈希表,哈希表里的数据是pair<K,V>类型Hash_table<K, pair<const K, V>, mapoft> _ht;};
};

七.实现map[]运算符重载

map的[ ]运算符重载,底层实现本质是调用了insert函数。然后通过insert函数返回的pair<iterator,bool>类型数据来找到Value值。

所以在实现[ ]运算符重载时,我们需要对哈希表里的insert进行改造,因为原来的insert的返回值是布尔值,我们需要pair类型返回值。

pair<iterator,bool> insert(const T& data){KeyOfT kt;Hashfunc hf;//仿函数可以使数据模//在插入之前,确认一下表里是否已经有了这个值,如果有了就不用插入了iterator it = find(kt(data));if (it!=end()){return make_pair(it,false);}if (n == _table.size()){//异地扩容,重新开空间size_t newSize = _table.size() * 2;vector<Node*> newtable;newtable.resize(newSize, nullptr);//不能再复用下面的方法,这样不好,因为就又重开空间,然后又要释放,//我们应该将原来的结点拿过来使用//所以我们遍历旧表,将旧表的结点拿过来,签到新表上for (size_t i = 0; i < _table.size(); i++){//扩容后,空间size变大了,有的数据就可能会存到不同的桶里了//拿下来的结点要重新计算放进哪个位置Node* cur = _table[i];//cur后面可能还有链接的结点while (cur){size_t hashi = hf(kt(cur->_data)) % newtable.size();Node* next = cur->_next;//头插到新表cur->_next = newtable[hashi];//头插 这个结点的 接到插入结点的前面对吧//那么next就接到newtavle[i]newtable[hashi] = cur;//往后走接着拿走cur = next;}//当前桶里的结点被拿光后,就置为空_table[i] = nullptr;}//这个新表就是我们想要的表,那么我们利用vector、的交换,让旧表和新表交换_table.swap(newtable);}size_t hashi = hf(kt(data)) % _table.size();Node* newnode = new Node(data);newnode->_next = _table[hashi];_table[hashi] = newnode;//将新结点头插到哈希桶里++n;return make_pair(iterator(newnode,this),true);}iterator find(const K& key){Hashfunc hf;KeyOfT kt;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kt(cur->_data) == key)return iterator(cur,this);elsecur = cur->_next;}return iterator(nullptr,this);}

哈希表的insert改造后,那么set和map里的insert都需要修改,因为底层用的就是调用用哈希表的insert。

在这里插入图片描述
这样修改是对的吗?有没有什么问题呢?
但是这时会出现一个问题,set里面的insert报错。这是为什么呢?
在这里插入图片描述
问题在于,我们之前让普通迭代变成const迭代器,而这里的pair<iterator,bool>中的iterator其实本质上是const_iterator。
是pair<const_itearto,bool>类型的。而哈希表里的insert返回的是普通迭代器,也就是pair<iterator,bool>类型的。这是两个不同的类型,无法直接将pair<iterator,bool>类型转换成pair<const_itearto,bool>类型的。所以会报错。
在这里插入图片描述
·
解决方法:

1.迭代器的拷贝函数是浅拷贝,我们不需要写,编译器自动生成的拷贝就可以用,编译器自动生成的拷贝函数只能实现普通迭代器拷贝给普通迭代器,const迭代器拷贝给const迭代器。(原理就是拷贝函数的对象类型就是调用这个函数的类型,当普通迭代器调用拷贝时,那么拷贝对象就是普通类型,当const迭代器调用拷贝时,那么拷贝对象就是const类型)
2.而我们需要的是让普通迭代器能够拷贝给const迭代器。所以我们需要自己增加拷贝函数。
3.库里的设计很妙,库里重新定义了一个iterator,作为拷贝对象,而这个iterator固定了就是普通的迭代器,不会随着调用对象而改变类型。所以当普通迭代器调用时,就会将普通iterator拷贝给它。当const迭代器调用时,就会将普通迭代器iterator拷贝给它。
4.所以我们需要对哈希表的迭代器添加拷贝构造。用普通迭代器iteartor作为拷贝对象。

struct HSIterator
{typedef  HashNode<T> Node;typedef HSIterator<K, T, T&, T*, KeyOfT, Hashfunc> iterator;typedef HSIterator<K, T,Ref,Ptr,KeyOfT, Hashfunc> Self;Node* _node;const Hash_table <K,T, KeyOfT,Hashfunc>* _pht;//没有取这个类里面的内嵌类型,就不需要加typenameHSIterator(Node* node, Hash_table<K, T, KeyOfT,Hashfunc>* hs)//hs是普通的类型:_node(node), _pht(hs){}//当传过来的哈希表指针是普通指针就走上面--权限的缩小//重载一个,当传过来的是const修饰的哈希表指针就走这里---权限的平移HSIterator(Node* node, const Hash_table<K, T, KeyOfT, Hashfunc>* hs)//hs是普通的类型:_node(node), _pht(hs){}//普通的拷贝构造就是 const迭代器拷贝给const迭代器,普通迭代器拷贝给普通迭代器//而我们写的拷贝构造可以同时支持两个,当调用对象是普通迭代器时,用普通迭代器拷贝,也就是相当于赋值//当调用对象是const迭代器时,也是用普通迭代器拷贝。这样就支持了普通迭代器转化成const迭代器了HSIterator(const iterator& it):_node(it._node),_pht(it._pht){}……………………………………………………};

这样处理后,我们再利用pair<iterator,bool>类型的构造函数,将普通迭代器转换成const迭代器。

1.先将insert返回类型利用ret接收
2.利用pair<iteartor,bool>构造将ret里的普通迭代器转换为const迭代器。

	pair<iterator,bool> insert(const K& key){//return _ht.insert(key);//set调用的insert传回来的pair<iterator,bool>类型的,pair<iterator,bool>与pair<const_iterator,bool>是两个不同的类型//正常的的迭代器拷贝构造我们不需要写,因为迭代器是指针,拷贝构造就是浅拷贝,编译器自动生成的拷贝就可以//但是这里我们需要自己写拷贝构造; 因为需要将pair<iteartor,bool>类型,转化成pair<const_iterator,bool>类型,如何转化的呢?pair<typename Hash_table<K, K, setoft>::iterator, bool> it = _ht.insert(key);return pair<iterator, bool>(it.first, it.second);}

最后insert的改造到这里就结束了,insert改造完后,就可以实现[ ]运算符重载了。

V& operator[](const K&key){pair<iterator, bool> ret = _ht.insert(make_pair(key,V()));//插入的数据是pair类型,要用make_pair构造return ret.first->second;}

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

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

相关文章

uniapp ui安装 阿里图标库使用 报错 Assignment to constant variable.

安装 ui uni-app官网 (dcloud.net.cn) &#xff08;一&#xff09;安装 pages.js配置 安装 sassnpm i sass -D 或 yarn add sass -D 安装 sass-loader npm i sass-loader10.1.1 -D 或 yarn add sass-loader10.1.1 -D安装 uni-uinpm i dcloudio/uni-ui 或 yarn a…

SpringMVC-拦截器

过滤器实现Filter接口&#xff0c;是处理Servlet请求的&#xff1b;而拦截器实现HanderInception接口&#xff0c;处理Spring-mvc请求的。 一、拦截器的基本使用 方式一&#xff1a; 方式二&#xff1a; 在经过步骤一直接可以到4 注意&#xff1a;ProjectInterceptor类 最好…

YOLOV8-DET转ONNX和RKNN

目录 1. 前言 2.环境配置 (1) RK3588开发板Python环境 (2) PC转onnx和rknn的环境 3.PT模型转onnx 4. ONNX模型转RKNN 6.测试结果 1. 前言 yolov8就不介绍了&#xff0c;详细的请见YOLOV8详细对比&#xff0c;本文章注重实际的使用&#xff0c;从拿到yolov8的pt检测模型&…

ping通但浏览器访问不了

ipconfig /renew ipconfig /flushdnshttps://mbd.baidu.com/newspage/data/dtlandingsuper?niddt_3086405504299374796

Shiro高级及SaaS-HRM的认证授权

Shiro在SpringBoot工程的应用 Apache Shiro是一个功能强大、灵活的&#xff0c;开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。越来越多的企业使用Shiro作为项目的安全框架&#xff0c;保证项目的平稳运行。 在之前的讲解中只是单独的使用shiro&…

SPSS探索性分析

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件可在个人主页—…

ElementUI之动态树+数据表格+分页

目录 前言 一.ElementUI之动态树 1.前端模板演示 2.数据绑定 2.1 通过链接获取后台数据 2.2 对链接进行绑定 2.3添加动态路由 2.4 配置路由 3.效果演示 二.数据表格动态分页 1.前端模板 2.通过JS交互获取后端数据 3 效果演示 前言 Element UI 是一个基于 Vue.js 的开…

1.4.C++项目:仿mudou库实现并发服务器之buffer模块的设计

一、buffer模块&#xff1a; 缓冲区模块 Buffer模块是一个缓冲区模块&#xff0c;用于实现通信中用户态的接收缓冲区和发送缓冲区功能。 二、提供的功能 存储数据&#xff0c;取出数据 三、实现思想 1.实现换出去得有一块内存空间&#xff0c;采用vector ,vector底层是一个…

华为智能企业上网行为管理安全解决方案(1)

华为智能企业上网行为管理安全解决方案&#xff08;1&#xff09; 课程地址方案背景需求分析企业上网行为概述企业上网行为安全风险分析企业上网行为管理需求分析 方案设计组网架构设备选型设备简介行为管理要点分析方案功能概述 课程地址 本方案相关课程资源已在华为O3社区发…

8个居家兼职,帮助自己在家搞副业

越来越多的人开始追求居家工作的机会&#xff0c;无论是为了获得更多收入以改善生活质量&#xff0c;还是为了更好地平衡工作和家庭的关系&#xff0c;居家兼职已成为一种趋势。而在家中从事副业不仅能够为我们带来额外的收入&#xff0c;更重要的是&#xff0c;它可以让我们在…

Vue 实现表单的增删改查功能及表单的验证

前言&#xff1a; 上一篇我们已经将前端表单的数据和后端的数据交互了&#xff0c;今天我们就继续开发功能来实现表单的增删改查功能及表单的验证 一&#xff0c;表单的增删改查功能 新增 去官网找模版&#xff1a; 1.1添加新增按钮&#xff1a; 1.2添加新增弹窗点击事件&am…

二叉树MFC实现

设有一颗二叉树如下&#xff1b; 这似乎是一颗经常用作示例的二叉树&#xff1b; 对树进行遍历的结果是&#xff0c; 先序为&#xff1a;3、2、2、3、8、6、5、4&#xff0c; 中序为&#xff1a;2、2、3、3、4、5、6、8&#xff0c; 后序为2、3、2、4、5、6、8、3&#xff1b…

基于Vue和Element UI实现前后端分离和交互

目录 前言 一、Element UI简介 1.Element UI是什么 2.Element UI的特点 二、项目搭建 1.创建一个SPA项目 2.安装 Element-UI 3.导入组件 4.创建登陆注册界面 登录组件---Login.vue 注册组件---Register.vue 定义组件与路由的对应关系 效果演示&#xff1a; 三、前…

Python中的正则表达式:常见问题与解决方案

正则表达式在Python中是一种非常强大的工具&#xff0c;用于处理文本数据。它可以帮助我们快速有效地进行模式匹配、搜索和替换。然而&#xff0c;在使用正则表达式时可能会遇到一些常见问题。本文将为您分享在Python中使用正则表达式时的常见问题与解决方案&#xff0c;并提供…

性能测试工具 — JMeter

一、JMeter准备工作 1、JMeter介绍 Apache JMeter 应用程序是开源软件&#xff0c;是一个 100% 纯 Java 应用程序。用于测试Web应用程序、API和其他网络协议的性能。它具有以下特点&#xff1a; 1. 开源免费&#xff1a;JMeter是Apache软件基金会下的一个开源项目&#xff0…

MySQL知识笔记——中级进阶之索引(实施工程师和DBA工作笔记)

在上一章中我们已经讲完了学习和实施工作中需要掌握的MySQL基础知识&#xff0c;但是在实际应用中这些基础只能让我们简单了解流程&#xff0c;以后的工作不只是简单的安装部署系统&#xff0c;我们还要将客户的数据导入数据库中才能完善系统的完整性和可使用性&#xff0c;接下…

LeetCode每日一题:2136. 全部开花的最早一天(2023.9.30 C++)

目录 2136. 全部开花的最早一天 题目描述&#xff1a; 实现代码与解析&#xff1a; 贪心 原理思路&#xff1a; 2136. 全部开花的最早一天 题目描述&#xff1a; 你有 n 枚花的种子。每枚种子必须先种下&#xff0c;才能开始生长、开花。播种需要时间&#xff0c;种子的生…

Selenium 浏览器坐标转桌面坐标

背景&#xff1a; 做图表自动化项目需要做拖拽操作&#xff0c;但是selenium提供的拖拽API无效&#xff0c;因此借用pyautogui实现拖拽&#xff0c;但是pyautogui的拖拽是基于Windows桌面坐标实现的&#xff0c;另外浏览器中的坐标与windows桌面坐标并不是一比一对应的关系&am…

基于微信小程序的同城家政服务预约系统(源码+lw+部署文档+讲解等)

文章目录 前言系统主要功能&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…

关于 自定义的RabbitMQ的RabbitMessageContainer注解-实现原理

概述 RabbitMessageContainer注解 的主要作用就是 替换掉Configuration配置类中的各种Bean配置&#xff1b; 采用注解的方式可以让我们 固化配置&#xff0c;降低代码编写复杂度、减少配置错误情况的发生&#xff0c;提升编码调试的效率、提高业务的可用性。 为什么说“降低…