【C++和数据结构】模拟实现哈希表和unordered_set与unordered_map

目录

一、哈希的概念与方法

1、哈希概念

2、常用的两个哈希函数

二、闭散列的实现

1、基本结构:

2、两种增容思路 和 插入

闭散列的增容:

哈希表的插入:

3、查找

4、删除

 三、开散列的实现

1、基本结构

2、仿函数Hash 

3、迭代器实现

4、增容和插入

5、查找 

6、删除

7、Clear和析构函数

四、哈希表模拟实现unordered_set和unordered_map


看这篇文章之前你需要对哈希表有一定了解,本文主讲代码实现 

一、哈希的概念与方法

1、哈希概念

哈希表和数组区别:哈希表是在数组的基础上按 映射关系放的 

比如我们身份证号就是个映射关系,比如看身份证号的前几位,就能知道你是哪个省的,后几位就能看出你的生日

注:

哈希:映射方式的算法

哈希表:使用哈希建立的数据结构


2、常用的两个哈希函数

注:由于除留余数法是目前很好的哈希函数,本文讲解用的全是除留余数法,故不用直接定址法

1. 直接定址法 --( 常用 )
 取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
面试题: 字符串中第一个只出现一次字符

2. 除留余数法 --( 常用 )
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
直接定址法是是 没有哈希冲突的,每个值都映射了一个唯一位置,但为什么要引入除留余数法,因为若数据分布不均匀,那么我们给每个值映射一个位置,可能会造成巨大的空间浪费。而除留余数法,不再是给每个值映射一个位置,是在限定大小的空间中将我们的值映射进去。 index = key % 空间大小

 除留余数法带来的问题:不同的值可能会映射到相同位置上去,导致哈希冲突。哈希冲突越多整体而言效率越低。

除留余数法的使用:

 如何解决哈希冲突呢?

 解决哈希冲突两种常见的方法是:闭散列开散列

1、闭散列——开放定址法(当前位置冲突了,则按规则再给你找个位置)

        a、线性探测     b、二次探测

2、开散列——拉链法哈希桶)(重点


二、闭散列的实现

先用哈希表的查找来引出基本结构的设计

 可见,删除会影响查找,可不可以这样考虑:直接遍历整个表查找一下有没有21?不行,哈希表就是为了效率而生的,你这么搞效率就是O(N)了,哈希表就没意义了。

注:哈希表的代码在HashTable.h中实现

1、基本结构:

这里的哈希表实现完全可以写为KV模型的,但是我们这里是为了模拟实现unordered_setunordered_map,故我们用K,T 

  • K就是按照关键字K来查找等
  • unordered_set对于T,会传入K,而unordered_map对于T,会传入pair<K,V>,故用T来表示对两种结构的通用
  • KeyOfT表示要取出的数据是K类型的,因为我们后续的比较都是用K类型的来直接比较,所以我们要取出T中的K,这一点利用仿函数来实现,(因为两种结构所传入的T不同)

哈希表中存放两个变量,为什么要用vector?首要原因是哈希表的本质就是数组,而vector符合是动态数组这一点,其次是方便,vector是自定义类型,它支持的resize等操作在我们实现哈希表过程中非常好用,并且它的析构也不用我们管

enum State
{EMPTY, //空EXIST, //存在DELETE //删除
};template<class T>
struct HashData
{T _data;State _state; //代表数据状态HashData():_state(EMPTY),_data(0){}
};template<class K, class T, class KeyOfT>
class HashTable
{typedef HashData<T> HashData;
private:vector<HashData> _tables; //哈希数组size_t _num = 0;	//表中存了多少个有效个数,不等于容量
};

代码中HashTable不写构造函数是因为_num初始给0了,而vector是自定义类型,它本身就有构造函数,所以没必要写构造函数 


2、两种增容思路 和 插入

闭散列的增容:

负载因子衡量哈希表满的程度,哈希表不敢让表太满,表一满,那对于哈希表插入等操作的效率是非常低的,所以引入负载因子

增容分两种思路:

1、传统思路 

  • a、开2倍大小的新表
  • b、遍历旧表的数据,重新计算在新表中位置
  • c、释放旧表

增容不能直接将旧表中的数据拷贝到新表中就完事了,而应该重新映射,若不重新映射,由于表的大小会变,旧表中的数据到新表中的数据会改变,故要重新映射 

 

2、简便思路

创建一个新的哈希表,利用哈希表已实现的Insert操作直接把原哈希表中每个数据插入到新哈希表中,注意临界条件,如果是初次增容只会resize开辟空间,详细操作见代码

哈希表的插入:

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。那如何寻找下一个空位置呢?怎么找利用线性探测和二次探测,怎么插入就要找空位置或者是被删除过的位置,这一点闭散列使用enum枚举状态做到的

规则:

  • a、线性探测(挨着往后找,直到找到空位置
  • b、二次探测(按 i^2,跳跃着往后找,直到找到空位置)

线性探测:

我们先计算出该数据映射到的位置,如果映射的位置已有数据,则继续挨着往后找,直到找到一个空位置(EMPTY)或一个被删除过的位置(DELETE),我们一定能找到一个位置的,因为闭散列的数组我们引入了负载因子,我们不可能让数组满了的。

其次要注意闭散列是一个数组结构,走到尾部还没找到一个位置就要回数组头部去找,针对此操作代码可以用以下两种写法(index代表位置):

//法一、
if (index == _tables.capacity())
{index = 0;
}
//法二、
index %= _tables.capacity();

 二次探测:

 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
置的方式就是挨着往后逐个去找,会导致一片一片的冲突,因此二次探测为了避免该问题,其让冲突的数据相对分散了,不会导致连片冲突效率更高

代码如下:

bool Insert(const T& data)
{KeyOfT koft;//1、增容:第一次插入或者负载因子>=0.7就要增容if (_tables.capacity() == 0 || _num * 10 / _tables.capacity() == 7){//A、增容——传统思路//vector<HashData> newtables;//size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;//newtables.resize(newcapacity);//开空间+自动初始化为0把旧空间数据拷贝到新空间中//for (size_t i = 0; i < _tables.capacity(); ++i)//{//	if (_tables[i]._state == EXIST)//	{//		size_t index = koft(_tables[i]._data) % newtables.capacity();//		while (newtables[index]._state == EXIST)//		{//			index++;//			if (index == newtables.capacity())//			{//				index = 0;//走到尾了就要返回头找位置//			}//		}//		newtables[index] = _tables[i];//	}//}//_tables.swap(newtables);//B、增容——简便思路HashTable<K, T, KeyOfT> newht;size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;newht._tables.resize(newcapacity);for (size_t i = 0; i < _tables.capacity(); ++i){if (_tables[i]._state == EXIST){newht.Insert(_tables[i]._data);//把原哈希表中每个数据利用Insert都插入到新哈希表中}}_tables.swap(newht._tables);//交换两者的vector}//1、线性探测//size_t index = koft(data) % _tables.capacity();//计算出要映射的位置//while (_tables[index]._state == EXIST)//{//	if (koft(_tables[index]._data) == koft(data))//	{//		return false;//如果存在相同的数据//	}//	++index;//	if (index == _tables.capacity())//	{//		index = 0;//	}//}//2、二次探测size_t start = koft(data) % _tables.capacity();size_t index = start;int i = 1;while (_tables[index]._state == EXIST){if (koft(_tables[index]._data) == koft(data)){return false;}index = start + i * i;++i;index %= _tables.capacity();}//插入数据_tables[index]._data = data;_tables[index]._state = EXIST;//用状态表示该位置已有数据++_num;		//有效数据个数++return true;
}

问题解释和知识回顾(理解代码):

 1、vector的resize

resize会开空间+初始化,而初始化什么值,你不传,他就会默认用这个类型的默认值,比如你vector中存的是int,那就默认初始化为0,你vector中存的是指针,默认初始化为nullptr,你vector中存的是string,默认初始化为空串

2、

增容完这个操作什么意思?符合现代写法

1、交换的是vector,那_tables不就是增容完的vector吗,那我后续操作还可以用_tables

2、newht的_tables就被换得了原vector的空间,而newht是个局部变量,出作用域就销毁,正好把我原vector的空间释放了

3、

KeyOfT koft;

这是什么?KeyOfT是模板参数,而它是为了取unordered_set和unordered_map中的key值的,本质上是利用仿函数实现了这一点,故你定义一个仿函数对象koft,利用koft调用仿函数operator()即可取出T类型数据中K类型的key值

4、

_num * 10 / _tables.capacity() == 7

判断负载因子为什么要这么写,因为整数相除不能得到0.7,其实转为double也是个方法,但是不推荐,直接*10不就行了嘛


3、查找

查找操作开头就已解释过

HashData* Find(const K& key)
{KeyOfT koft;size_t index = key % _tables.capacity();while(_tables[index]._state != EMPTY){//只要是存在和删除状态就要持续往下找if (koft(_tables[index]._data) == key){if (_tables[index]._state == EXIST)return &_tables[index];//值相等且为存在状态elsereturn nullptr;//值相等但为删除状态,说明被删除了}++index;//没找到继续往后找index %= _tables.capacity();}return nullptr;
}

4、删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44查找起来可能会受影响。因此 线性探测采用标记的伪删除法来删除一个元素
	bool Erase(const K& key){HashData* ret = Find(key);if (ret){ret->_state = DELETE;//用状态代表删除状态--_num; //--有效元素个数return true;}else{return false;}}

测试开散列代码:

template<class K>
struct SetKeyOfT
{const K& operator()(const K& key){return key;}
};void TestCloseHash()
{CLOSEHASH::HashTable<int, int, SetKeyOfT<int>> ht;ht.Insert(2);ht.Insert(4);ht.Insert(14);ht.Insert(24);ht.Insert(26);ht.Insert(16);ht.Erase(14);ht.Erase(2);CLOSEHASH::HashData<int>* data = ht.Find(4);
}

用线性探测测试代码: 

 用二次探测测试代码:

因为闭散列没有开散列好,所以这里闭散列简单实现下即可,对于更进一步的迭代器、和实现unordered_set和unordered_map等等操作我们都是用开散列实现,开散列才是重中之重 


 三、开散列的实现

开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中

这里的开散列就像个数组和链表的融合体,但开散列本质还是个数组


1、基本结构

template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{typedef HashNode<T> Node;private:vector<Node*> _tables; //使用vector一定要包含std库,不然使用不了size_t _num = 0;      //有效元素的个数,不是容量
};

细品:(_tables)表是一个指针数组,是这个数组中存了第一个节点的地址  

2、仿函数Hash 

 为什么模板参数多了个Hash?

它是个仿函数,他可以针对string、自定义类型等不能取模的类型,让他们能支持取模运算,因为对于开散列也涉及取模运算,它要求数据的映射位置

若用string类型就无法取模,如果key的类型是一个结构体呢,是不是也不能取模,也就是进来的key不一定能取模

 因为string不支持直接取模,故再写一个仿函数_Hash,然后内部使用_Hash即可,用仿函数来支持取模运算,默认情况下使用_Hash<K>,但遇到string我们需显式地传_HashString

那么_HashString的思路是什么?拿字符串的第一个字母的ASCII码取模可以吗?不好,因为若这些字符串的第一个字母相同,就容易产生冲突,故我们把所有字母ASCII码值加起来(因为哈希表是取模后映射位置,都用ASCII码来计算,就会有对应的映射位置)

 那怎么使用这个仿函数呢?

HashTable中写个HashFunc直接把数据转换成能取模的,因为这个操作需要频繁使用

size_t HashFunc(const K& key)
{Hash hash;return hash(key);//利用仿函数把key值转为整形
}

存在的问题:

 因为不同的字符串,相加后可能是相同的,那放在同一位置会引起哈希冲突,故要把字符串转成整形等,用字符串哈希算法来算,比如出色的BKDR Hash算法字符串每次乘以131即可,则字符串算出来的值就不那么容易冲突了(也可以乘以31,1313,13131,131313..),但是无论如何一定会有可能发生冲突,因为字符串在不限定长度的情况下是无限长的,我们要做的就是降低冲突概率

应用BKDR后,就会发现不冲突了,它大大的降低冲突性

那对于结构体想取模怎么办?比如一个人的信息是一个结构体,那我们就可以找人的信息中有代表项的东西,比如人的电话号码,是个字符串,就能唯一的代表人。用身份证也行。那如果这个人不能用电话号码和身份证等,我们用名字作为代表项码?不好,名字相同的人很多,很容易产生冲突,故我们可以用名字+另一个代表项,比如名字+家乡地址作为代表项,算出映射位置

注:

因为string类型会经常去做key,我们干脆让string作为默认的仿函数,一个模板类型要对一个参数作特殊处理就要用到模板特化,这下即使不传参数,也能处理好string类型的哈希表使用,就是在于它变成默认支持的了

template<class K>
struct _Hash
{const K& operator()(const K& key){return key;//可以取模的直接返回即可}
};//特化
template<>
struct _Hash<string>
{size_t operator()(const string& key){//运用BKDR Hash算法size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131; //BKDRhash += key[i];//字符串中每个字母ASCII码值相加}return hash;}
};template<class K, class T, class KeyOfT, class Hash = _Hash<K>>
class HashTable

注意:上面模板参数class Hash = _Hash<K>表示默认会调用_Hash<K>,那这个默认一个是string可以默认调用,另一个就是能正常取模的类型可以正常调用,也就是你调用HashTable时不传这个Hash参数就可以帮你自动调用这个默认的,你若不用模板特化的话string类型的是不会帮你默认调用的(但我们说了我们的哈希表是为了给unordered_set和unordered_map使用的,也就是上层使用的,你下层哈希表的实现就不用写为class Hash = _Hash<K>了,这一点在模拟实现unordered_set和unordered_map时,它们俩会写的,也就是模拟实现时这里HashTable的参数只会写为class Hash即可)

如果我不想在创建对象时还要传仿函数(说的是取模的仿函数),但是对于string还能正常调用到它的仿函数,那就可以用模板特化,一个模板类型要对一个参数作特殊处理就要用到模板特化,当K是string类型的时候,就会调用_Hash的模板特化,正常处理string取模问题

但是再来一个类型你也可以写成模板特化(但它变成默认的模板参数前提应该是这个类型在某个场景下会被频繁使用),但如果不是频繁使用的,就没必要整成默认的,直接单独写一个仿函数,显示传参数即可


3、迭代器实现

关于闭散列的哈希表的迭代器,就和list迭代器的思路相仿(建议先把list的迭代器的模拟实现看明白),不过在此基础上还要额外考虑

这里最需要说明的是operator++,其他的操作容易理解

 哈希桶的迭代器operator++思路:

迭代器不需要按照插入顺序迭代,底层实现也没有考虑,但是我们可以思考一下如果按插入的顺序迭代遍历要如何实现 :

为了我们能找到下一个桶,若我们使用vector,但我就一个迭代器,怎么用vector呢

我怎么知道我在哪个桶呢?可以再计算下此时的映射位置,即可以通过桶中的值,计算我是哪个桶。

如果这里直接传vector,那HashFunc就调用不到了,所以干脆给个哈希表,并且是哈希表指针,就能用HashFunc

总结:

利用哈希表,也就是在迭代器中再创建个哈希表对象的指针,利用哈希表找下一个桶,因为我哈希表里面有vector啊,vector里面存的是头指针,利用这个头指针不就能找到下一个桶了

注:哈希表迭代器不支持--操作,因为它是单向迭代器,也就是没有rbegin和rend等等

//前置声明:为了让哈希表的迭代器能用哈希表
template<class K, class T, class KeyOfT, class Hash>
class HashTable;		template<class K, class T, class KeyOfT, class Hash>
struct __HashTableIterator
{typedef __HashTableIterator<K, T, KeyOfT, Hash> Self;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HashNode<T> Node;Node* _node;//迭代器中存的是节点指针HT* _pht;//对象的指针__HashTableIterator(Node* node, HT* pht):_node(node),_pht(pht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self operator++(){if (_node->_next){//如果还能在一个桶中,就直接在一个桶中往后走(单链表)_node = _node->_next;}else{// 如果一个桶走完了,要往下找到下一个桶继续遍历KeyOfT koft;//先计算我当前是在哪个桶size_t i = _pht->HashFunc(koft(_node->_data)) % _pht->_tables.capacity();++i;//下一个桶for (; i < _pht->_tables.capacity(); ++i){	//找不为空的桶Node* cur = _pht->_tables[i];if (cur){	//如果这个桶不为空_node = cur;return *this;//迭代器++返回的是迭代器本身}}_node = nullptr;//如果没有找到有数据的桶,则指针置为空,与end()相符return *this;}}bool operator!=(const Self& s){return _node != s._node;}
};

哈希表中的begin()和end()实现:

	typedef __HashTableIterator<K, T, KeyOfT, Hash> iterator;//begin()返回第一个不为空的桶的第一个节点iterator begin(){for (size_t i = 0; i < _tables.capacity(); ++i){if (_tables[i]){return iterator(_tables[i], this);//找到了则构造匿名对象返回}}return end();//每个桶中都没找到则返回end()}iterator end(){return iterator(nullptr, this);}

4、增容和插入

增容就涉及重新映射元素位置的问题,那我们就取出原哈希表中每个桶中的元素,然后重新计算它在新表中的映射位置,那放到新表中有冲突的元素怎么办?头插即可,和插入的思路一样,或者说增容的思路就包含了插入的思路。我们会一个一个桶往后走,那每一个桶都是一个单链表,遍历这个单链表用while,不断计算桶中每个元素在新表中的映射位置,遍历桶我们相当于遍历_tables

插入的关键是这个冲突的值是尾插还是头插到对应位置呢?按理说,冲突的数据的顺序是无所谓的,故头插尾插都可以,也就是达到的效果一样,但是尾插还要先找到尾再插入,故这里用头插实现

假设表容量capacity为10

pair<iterator, bool> Insert(const T& data)
{KeyOfT koft;//1、判断是否增容if (_tables.capacity() == _num){	//开散列的实现平衡因子为1就增容且第一次插入也会增容size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;vector<Node*> newtables;newtables.resize(newcapacity, nullptr);//给新的vector开新空间+初始化//重新计算旧表的数据在新表中的映射位置for (size_t i = 0; i < _tables.capacity(); ++i){	//如果是第一次的增容不会进for循环的,故不用担忧表的初始数据是否为nullptr//哈希表中每一个桶都是一个单链表,故考察单链表的头插Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t index = HashFunc(koft(cur->_data)) % newtables.capacity();//重新计算映射位置//头插cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;//映射到新表后置为空}_tables.swap(newtables);//新旧表的vector交换}size_t index = HashFunc(koft(data)) % _tables.capacity();//计算新的映射位置//1、先查找这个元素是否在哈希表中Node* cur = _tables[index];//知道映射位置就能确定是哪个桶while (cur){if (koft(cur->_data) == koft(data))return make_pair(iterator(cur, this), false);//找到相同数据则插入失败elsecur = cur->_next;}//2、头插到这个桶中Node* newnode = new Node(data);//开辟新节点//头插newnode->_next = _tables[index];_tables[index] = newnode;++_num;//哈希表中有效元素个数++return make_pair(iterator(newnode, this), false);
}

 1、Insert的返回值底层实现就是pair<iterator, bool>

插入数据:

若要插入的数据已存在于哈希表中,则返回已存在的数据的位置,并返回false,即代码中的make_pair(iterator(newnode, this), false),这里用的是iterator的构造函数,因为构造函数初始化列表有一个哈希表指针,而this就是我本身的哈希表指针( HashTable<K, T, KeyOfT, Hash>*类型的)

若要插入的数据不存在于哈希表中,则返回这个要插入数据的位置,并返回true,即代码中的make_pair(iterator(cur, this), true);


5、查找 

Node* Find(const K& key)
{KeyOfT koft;size_t index = HashFunc(key) % _tables.capacity();//先计算映射位置Node* cur = _tables[index];while (cur){if (koft(cur->_data) == key)return cur;elsecur = cur->_next;}return nullptr;//走遍桶都没找到则返回空
}

6、删除

 开散列的删除,就相当于单链表的删除,故要找前一个节点,而删除的前一步是找到你要删除的数据在不在

bool Erase(const K& key)
{assert(_tables.capacity() > 0);//有空间才能删KeyOfT koft;size_t index = HashFunc(key) % _tables.capacity();Node* cur = _tables[index];Node* prev = nullptr;//记录cur的前一位置while (cur){if (koft(cur->_data) == key){if (prev == nullptr){	//如果是头删_tables[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_num;return true;}else{prev = cur;cur = cur->_next;}}return false;//要删除数据根本不存在
}

7、Clear和析构函数

虽然哈希表不用我们写构造函数,但是因为哈希表中每个节点都是动态开辟的,故我们要释放下每个节点,遍历每个桶,每个桶都是单链表,即考察单链表的节点释放

	~HashTable(){Clear();//vector不用我们释放,因为它是自定义类型,哈希表要清理时,vector也会自动清理}void Clear(){for (size_t i = 0; i < _tables.capacity(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;//清空完数据后置为nullptr}}

遗留的问题: 

 当大量的数据冲突,这些哈希冲突的数据就会挂在同一个链式桶中 ,查找时效率就会降低,所以开散列-哈希桶也是要控制哈希冲突的。如何控制呢?通过控制负载因子,不过这里就把空间利用率提高一些,负载因子也可以高一些,一般开散列把负载因子控制到1,会比较好一些  


四、哈希表模拟实现unordered_set和unordered_map

 对于unordered_set和unordered_map第二个模板参数是个通过哈希来比较的(我们实现的是把这个参数放在第四个模板参数),万一我的unordered_set的模板参数K是个结构体类型,我不能直接取模就直接完蛋了,所以要传一个仿函数给哈希表

总结:这个默认的模板参数是利用上层模拟实现用的,你哈希表的底层实现就写为class Hash即可,我上层使用的时候会直接传给你

MyUnordered_set.h中实现unordered_set的

#pragma once
#include"HashTable.h"
using namespace OPENHASH;namespace mz
{using OPENHASH::_Hash;template<class K, class Hash = _Hash<K>>class unordered_set{struct SetKOfT{const K& operator()(const K& k){return k;}};public://加typename是告诉编译器这是个类型,你先让我过,等实例化了再去找//因为模板没实例化它是不接受你用模板里面的一个类型,故要用typename typedef typename HashTable<K, K, SetKOfT, Hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& k){return _ht.Insert(k);}private:HashTable<K, K, SetKOfT, Hash> _ht;};void test_unordered_set(){unordered_set<int> s;s.insert(1);s.insert(5);s.insert(4);s.insert(2);unordered_set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;}
}

 

MyUnordered_map.h中实现unordered_map的:

#pragma once
#include"HashTable.h"
using namespace OPENHASH;namespace mz
{using OPENHASH::_Hash;template<class K, class V, class Hash = _Hash<K>>//一般模板参数都是由上一层来控制的class unordered_map{struct MapKOfT {const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<K,V>, MapKOfT, Hash>::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){//unordered_map的operator[]是给key值返回v的引用//底层实现用的是哈希表的Insert来实现,在介绍使用时讲过这点pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}private:HashTable<K, pair<K, V>, MapKOfT, Hash> _ht;//底层是个哈希表};void test_unordered_map(){unordered_map<string, string> dict;dict.insert(make_pair("factual", "真实的"));dict.insert(make_pair("fringe", "侵犯"));dict.insert(make_pair("intermittent", "间歇的"));dict["prerequisite"] = "先决条件";dict["reduce to"] = "处于";//unordered_map<string, string>::iterator it = dict.begin();auto it = dict.begin();while (it != dict.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;}
}


 

遗留的问题: 

很明显我们实现的unordered_map和unordered_set排出来的数据并不是有序的,但std库里面会自动排序的

思路:再建立一个链表存储数据,尾插,迭代器遍历时,遍历链表,就可以保持遍历有序,那么主要问题是 遍历时保持插入的顺序,这样结构会复杂,但迭代器会变简单,定义两个指针,一个next指针用来挂桶,另一个prev指针用来遍历

这个知识了解思路即可

进一步改进:

 如果表的大小是一个素数,冲突率会有一定的降低,素数是只能被1和本身所整除的,那怎么保证是个素数呢?我们可以提供一个素数表,数字后面加ul,表示无符号的长整形,即unsigned long,这个素数表从头到尾基本上每个数据都是前一个的2倍,而最大的数接近整数最大值,【注:这个表是STL中的,其经过研究具有一定的优越性】

怎么用这个表?

在开辟表大小时调用这个素数表,从头遍历这个素数表,找到比我当前要开辟的大小大的即可

完整源码:

http://t.csdnimg.cn/XTY7Y

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

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

相关文章

React 中 keys 的作用是什么?

目录 前言&#xff1a;React 中的 Keys 的重要性 为什么 Keys 重要&#xff1f; 详解&#xff1a;key 属性的基本概念 用法&#xff1a;key 属性的示例 解析&#xff1a;key 属性的优势和局限性 优势&#xff1a; 局限性&#xff1a; key 属性的最佳实践 稳定的唯一标…

代码随想录二刷 Day46

10背包&#xff1a; 二维内侧与外侧都是正序遍历&#xff0c;二维的内侧与外侧是背包还是物品无所谓&#xff1b; 10背包&#xff1a; 一维外侧是正序&#xff0c;内侧是倒序&#xff1b; 目的是为了一个物品只选取一次&#xff1b;一维内侧一定要是背包&#xff1b;原因我想了…

SQL关于日期的计算合集

前言 在SQL Server中&#xff0c;时间和日期是常见的数据类型&#xff0c;也是数据处理中重要的一部分。SQL Server提供了许多内置函数&#xff0c;用于处理时间和日期数据类型。这些函数可以帮助我们执行各种常见的任务&#xff0c;例如从日期中提取特定的部分&#xff0c;计…

【2021研电赛】基于动态无线充电技术的自动驾驶小车

本作品介绍参与极术社区的有奖征集|分享研电赛作品扩大影响力&#xff0c;更有重磅电子产品免费领取! 参赛单位&#xff1a;北京交通大学 作品简介 近年来&#xff0c;电动汽车的发展得到了很多国家和车企的大力支持&#xff0c;但其仍然存在充电时间长、充电设施不齐全等问…

迷你洗衣机哪个牌子好又实惠?小型洗衣机全自动

现在洗内衣内裤也是一件较麻烦的事情了&#xff0c;在清洗过程中还要用热水杀菌&#xff0c;还要确保洗衣液是否有冲洗干净&#xff0c;还要防止细菌的滋生等等&#xff0c;所以入手一款小型的烘洗全套的内衣洗衣机是非常有必要的&#xff0c;专门的内衣洗衣机可以最大程度减少…

SpringMVC(三)获取请求参数

1.1通过ServletAPI获取 SpringMVC封装的就是原生的servlet 我们进行测试如下所示&#xff1a; package com.rgf.controller.service;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.…

学习MAVEN

MAVEN的详细介绍和作用、意义 好的&#xff0c;小朋友们&#xff0c;我们今天来聊聊一个非常神奇的工具箱&#xff0c;它的名字叫做Maven! &#x1f31f; 1. **神奇的工具箱Maven**: Maven就像是一个神奇的工具箱&#x1f9f0;&#xff0c;它可以帮助大人们把他们的电脑工…

【Docker】Dockerfile常用指令

参考官方文档&#xff1a;https://docs.docker.com/engine/reference/builder/ Dockerfile常用指令 指令说明from基础镜像&#xff0c;当前镜像基于&#xff08;依赖&#xff09;哪个镜像maintainer镜像的维护者和邮箱run镜像构建时需要执行的命令workdir镜像的工作目录expos…

基于springboot实现基于Java的超市进销存系统项目【项目源码+论文说明】

基于springboot实现基于Java的超市进销存系统演示 摘要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;超市进销存系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#x…

最详细STM32,cubeMX外部中断

这篇文章将详细介绍 cubeMX外部中断的配置&#xff0c;实现过程。 文章目录 前言一、外部中断的基础知识。二、cubeMX 配置外部中断三、自动生成的代码解析四、代码实现。总结 前言 实验开发板&#xff1a;STM32F103C8T6。所需软件&#xff1a;keil5 &#xff0c; cubeMX 。实…

09 创建型模式-建造者模式

1.建造者模式介绍&#xff1a; 建造者模式 (builder pattern), 也被称为生成器模式 , 是一种创建型设计模式 定义: 将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不 同的表示。 2.建造者模式要解决的问题 建造者模式可以将部件和其组装过程分开&am…

【Unity程序技巧】公共Update管理器

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

【29】c++设计模式——>策略模式

策略模式 C中的策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它允许在运行时选择算法的行为。策略模式通过将算法封装成独立的类&#xff0c;并且使它们可以互相替换&#xff0c;从而使得算法的变化独立于使用算法的客户端。 策略模式通…

图像语义分割 pytorch复现DeepLab v1图像分割网络详解以及pytorch复现(骨干网络基于VGG16、ResNet50、ResNet101)

图像语义分割 pytorch复现DeepLab v1图像分割网络详解以及pytorch复现&#xff08;骨干网络基于VGG16、ResNet50、ResNet101&#xff09; 背景介绍2、 网络结构详解2.1 LarFOV效果分析 2.2 DeepLab v1-LargeFOV 模型架构2.3 MSc&#xff08;Multi-Scale&#xff0c;多尺度(预测…

Matlab论文插图绘制模板第122期—函数折线图(fplot)

本期分享的是函数折线图的绘制模板。​ 所谓函数折线图&#xff0c;就是将自定义线函数进行可视化表达​。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。有需要的朋友可以关注同名公号【阿昆的…

【JavaEE】网络编程---TCP数据报套接字编程

一、TCP数据报套接字编程 1.1 ServerSocket API ServerSocket 是创建TCP服务端Socket的API ServerSocket 构造方法&#xff1a; ServerSocket 方法&#xff1a; 1.2 Socket API Socket 是客户端Socket&#xff0c;或服务端中接收到客户端建立连接&#xff08;accept方法&…

浅谈兼容性测试的关键步骤

兼容性测试是确保应用程序在多样化的技术环境中正常运行的关键步骤。它有助于提高用户满意度&#xff0c;扩大市场覆盖范围&#xff0c;同时确保法规合规性。通过正确执行兼容性测试&#xff0c;企业可以确保其应用程序在各种平台上提供一致的卓越用户体验&#xff0c;从而增强…

#电子电器架构 —— 车载网关初入门

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 PS:小细节,本文字数7000+,详细描述了网关在车载框架中的具体性能设置。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 没有人关注你。也无需有人关注你。你必须承认自己的价值,你不能站在他…

现在游戏出海有多少优势?

国内游戏市场趋于饱和&#xff0c;但是国外市场潜力仍然可观&#xff0c;因此很多人选择游戏出海&#xff0c;那么现在游戏出海有多少优势呢&#xff1f; 1、市场潜力 全球游戏市场潜力巨大&#xff0c;增长迅速。中国游戏公司具有强大的研发能力和创新能力&#xff0c;能够开…

在edge浏览器中安装好了burp的ca证书,浏览器依旧不能访问https的原因

在edge浏览器中安装好了burp的ca证书&#xff0c;浏览器依旧不能访问https的原因 1.SwitchyOmega代理插件设置2.CA证书方法1方法2 1.SwitchyOmega代理插件设置 严格安装以下图片执行&#xff0c;不可少写或多写 2.CA证书 方法1 下载好证书&#xff0c;先导入到edge浏览器的中…