unordered系列的map\multimap\set\multiset的介绍与模拟实现

目录

unordered系列的关联式容器的简单介绍

unordered_map的模拟实现

底层结构

经过修改过后的开散列版本的HashTable的整体代码

unordered_map的基础框架

unordered_map的迭代器、begin()、end()函数

unordered_map的Insert函数

unordered_map的operator[]函数

对以上接口的阶段性测试

unordered_map的Erase函数

unordered_map的Find函数

unordered_map的整体代码

unordered_set的模拟实现

unordered_set的整体代码

常见面试题:一个类型K去做set和unorder set的模板参数有什么要求?

unordered_multimap和unordered_multiset的模拟实现


unordered系列的关联式容器的简单介绍

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到Log(2N),即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率并不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器。

从使用方式上讲,unordered系列的关联式容器和非unordered系列的相比,没有太多区别。(但注意,它们的特性和底层结构是大不相同的)

那从使用方式上讲有哪些区别呢?区别在于:

1.因为我们知道map这些容器的底层是红黑树,红黑树又是搜索树,所以map这些非unordered系列的关联式容器中的数据都是有序的,而unordered系列的关联式容器和它们的区别在于容器中的数据是无序的。(unordered的翻译就表示无序)

2.如下图红框处所示,非unordered系列的关联式容器的迭代器是都单向迭代器,只支持operator++,不支持operator--,所以它们也没有rbegin()、rend()等接口;而map这些非unordered系列的关联式容器的迭代器是都双向迭代器。

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第1张图片

3.如下图,因为unordered系列的关联式容器的底层结构就是通过开散列的方式所实现的哈希表,因此unordered系列的关联式容器多了以下的这些接口,咱们下面就介绍几种常用的。

(如果对通过开散列的方式所实现的哈希表感觉很陌生,建议先回顾一下文章<<哈希的介绍以及哈希表的模拟实现>>)

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第2张图片

3.1、bucket_count函数用于返回一个size_t类型值,该值表示vector中所有桶(桶也就是链表)的个数(包含空桶),也就是vector的size;

3.2、bucket_size(i)函数用于返回一个size_t类型的值,该值表示第 i 号桶中有几个节点(说一下哈希桶中的节点Node类有一个表示数据的_data成员);

3.3、bucket(x)函数用于找一个T类数据x在几号桶内,如果是unordered_map,则不需要T是pair类型,只需要T是pair的first成员的类型即可。

3.4、load_factor函数用于返回一个float类型的值,该值表示目前哈希表中的负载因子。

3.5、max_load_factor函数如下图,有两种版本。第一个用于返回一个float类型的负载因子,该负载因子并不表示目前哈希表中的负载因子的值,而是一个基准值,表示达到了这个基准值哈希表就需要扩容;第二个用于设置基准值,将你传过去的参数设置为基准值。

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第3张图片

3.6、rehash(n)函数用于给哈希表扩容,n是size_t类型的数据,表示vector的哈希桶的个数,如果n大于目前vector的哈希桶的个数,那么就将目前vector的哈希桶的个数增加到n个并重新完成哈希映射(也叫重新散列);如果n小于目前vector的哈希桶的个数,则什么也不会做。

3.7、reserve(n)函数用于给哈希表扩容,也就是让vector中的哈希桶的数量变多,但reserve和上面的rehash的用法大不一样。哈希表的扩容逻辑是看当前的负载因子是否达到一个基准值,如果达到了就扩容,反之不用扩。因为负载因子=哈希表中的节点个数/哈希桶的个数,又因为基准值也称为最大负载因子,所以最大负载因子=哈希表中最多能存储的节点个数/哈希桶的个数,结合计算负载因子和基准值的公式仔细思考可以得出一个结论:因为哈希桶的个数不变,所以【当前的负载因子如果达到了基准值就扩容,反之不用扩】这句话就等价于【当前哈希表中的节点个数如果达到了哈希表中最多能存储的节点个数就扩容,反之则不用扩容】,所以哈希表的扩容逻辑就转化成了【当前哈希表中的节点个数如果达到了哈希表中最多能存储的节点个数就扩容,反之则不用扩容】,reserve(n)里的n就表示节点的个数,如果n大于哈希表中最多能存储的节点个数,则vector中的哈希桶的个数将增加并强制重新散列,至于增加几个哈希桶就看库里是怎么设计的了;如果n小于哈希表中最多能存储的节点个数,则该函数什么也不会做。因此不要认为该函数是直接把哈希桶的数量变成n个,这是非常错误的,正确的理解方式为:n表示哈希桶中节点的个数,假如我有n个数据(节点类Node有一个_data成员表示数据)要插进哈希表,则哈希表会根据自己的逻辑算出应该创建多少个哈希桶,通过这样完成哈希表的扩容,注意n是size_t类型。

说一下,因为unordered系列的关联式容器的底层都是哈希表,在<<哈希的介绍以及哈希表的模拟实现>>一文中说过,哈希表的Insert在扩容时是很坑的,因为不但要扩容,还要将所有节点重新进行哈希映射,这就大大影响了哈希表的Insert的效率,因此,如果提前知道了要插入的数据的总数,先使用reserve函数能极高的提高Insert的效率,因为不用频繁的扩容以及频繁的进行哈希映射了。

 从使用方式上讲,除了上面的差异外,unordered系列的关联式容器在其余部分几乎和非unordered系列完全一致,就不再赘述,请参考<>一文。 

问题:基于上面的比较,非unordered系列的关联式容器比unordered系列的关联式容器更优秀,为什么要存在unordered系列的关联式容器呢?

答案:如果有大量的数据时,unordered系列的关联式容器在增删查改时的效率更优秀。

如何证明上面的答案呢?以下代码可以检测这两类容器各自在增删查改大量数据时所花的时间,通过对比谁所花的时间更少即可判定出谁更优秀。

#include
using namespace std;
#include
#include
#include
#include
#include


void test1()
{
	int n = 1000000;
	vector v;
	v.reserve(n);
	srand(time(0));
	for (int i = 0; i < n; ++i)
	{
		//v.push_back(rand());  
		v.push_back(rand() + i);  //因为rand最多生成3w多个随机数,为了生成更多的随机数,这里通过+i,因为i是不同的,所以就能生成更多的随机数
	}

	//从begin1到end1是set插入1w个数据所花的时间
	size_t begin1 = clock();
	set s;
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();

	//从begin2到end2是unordered_set插入1w个数据所花的时间
	size_t begin2 = clock();
	unordered_set us;
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();

	cout << "size:" << s.size() << endl;//因为rand()函数在生成大量随机数时可能会生成重复值,所以这里是为了看看set和unordered_set中到底插入了多少个值

	cout << "set insert:" << end1 - begin1 << endl;
	cout << "unordered_set insert:" << end2 - begin2 << endl;

	//从begin3到end3是在set中把每个数据都查找一次后所共花的时间
	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();

	//从begin4到end4是在unordered_set中把每个数据都查找一次后所共花的时间
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();

	cout << "set find:" << end3 - begin3 << endl;
	cout << "unordered_set find:" << end4 - begin4 << endl;

	//从begin5到end5是在set中把每个数据都删除所共花的时间
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();

	//从begin6到end6是在unordered_set中把每个数据都删除所共花的时间
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();

	cout << "set erase:" << end5 - begin5 << endl;
	cout << "unordered_set erase:" << end6 - begin6 << endl;
}

void main()
{
	test1();
}


上面代码经过多次运行,可得到这样的结果,如下图。通过运行结果可以证明上面的答案是正确的。

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第4张图片

上面只进行了实践,但并没有理论知识支撑,因此现在就有一个问题:为什么有大量的数据时,unordered系列的关联式容器在增删查改时的效率对比非unordered系列的关联式容器,会更优秀呢?

答案:unordered系列的关联式容器之所以效率比较高,是因为其底层就是通过开散列的方式所实现的哈希(散列)表。理解了<<哈希的介绍以及哈希表的模拟实现>>一文后,我们知道哈希表的Insert是不需要像红黑树一样频繁的变色或者旋转的,哈希表的Insert唯一的消耗就是在扩容时需要对所有元素进行重新映射,这些消耗相对于红黑树的频繁旋转来说是更低的,因为即使哈希表的Insert在扩容时消耗很大,但如果插入的数据量已经很大后,扩容扩了2倍后是不需要频繁扩容的,Insert也就没有什么消耗了,因此在有大量的数据时,unordered系列的关联式容器在Insert的效率上比非unordered系列的关联式容器更优秀;而如果数据量很小的话,重新映射的消耗也就不算大了,经过实验发现这时Insert的效率依然比红黑树稍微高一点,所以总结就是unordered系列的关联式容器在Insert上的效率上更优秀;

而对于Find而言,在<<哈希的介绍以及哈希表的模拟实现>>一文中我们证明过哈希表的Find的时间复杂度是O(1),而红黑树的Find的时间复杂度是O(logN),所以unordered系列的关联式容器在Find的效率上也更优秀;

对于Erase而言,红黑树的删除比插入的效率还低,因为好歹红黑树在插入时不需要频繁的旋转,而删除是可能需要频繁旋转,一直旋转到根节点的,所以效率极低;而哈希表的Erase是没有什么消耗的,先找到目标节点,上一段说过查找的时间复杂度为O(1),然后剩下的逻辑就是链表断开连接,也是个常数次O(1)的消耗,因此哈希表的Erase效率是远远高于红黑树的,所以unordered系列的关联式容器在Erase的效率上也更优秀。

unordered_map的模拟实现

底层结构

在上文中说过,unordered系列的关联式容器的底层都是通过开散列的方式所实现的哈希表。然后要知道的是在<<哈希的介绍以及哈希表的模拟实现>>一文中咱们实现的哈希表是纯粹的哈希表,该哈希表不能直接拿来封装unordered系列的关联式容器,为什么呢?原因有很多,比如在该哈希表的Insert函数中是没有实现unordered_map所需要的去重逻辑的,因此首先要对<<哈希的介绍以及哈希表的模拟实现>>一文中的通过开散列的方式实现的哈希表OpenHash::HashTable进行稍微的修改。哪些位置需要修改呢?

(以下内容建议结合<<哈希的介绍以及哈希表的模拟实现>>一文中的通过开散列的方式所实现的哈希表进行理解、比对)

1,在<<哈希的介绍以及哈希表的模拟实现>>一文中,HashTable只有两个模板参数,分别是class T和class Hash,现在首先给HashTable增加一个模板参数class K来表示pair的first成员的类型。为什么呢?我们知道通过HashTable封装unordered系列的关联式容器时,T一定是pair,而因为对于unordered_map来说,在调用Find函数查找时或者调用Erase函数删除时,需要的参数的类型是pair的first成员的类型K,而unordered_map的Find函数就是调用HashTable的Find函数、unordered_map的Erase函数就是调用HashTable的Erase函数,因此如果HashTable光有一个模板参数T,即pair,是拿不到pair的first成员的类型的,Find函数的参数的类型也就无法表示。

2,根据上一段可知是还需要把Find和Erase函数的参数从T类型变成K类型的。

3,还要在定义HashTable类模板的地方模板,将template>变成template>,注意这里并不是说要对仿函数类hashfunc的实现进行修改,不要理解错了。为什么要这么做呢?因为我们知道通过HashTable封装unordered系列的关联式容器时,T一定是pair,而仿函数类hashfunc对于自定义类型,只支持将string类的对象转化成size_t类的对象,并不支持转化pair,而使用者定义unordered_map的对象时,K一般都是内置类型,也就支持将K转化成size_t了。可能有人会说:【如果我的K就是除string外的自定义类型呢?】我想说的是:这就属于非主流的情景了,此时就需要用户自行编写合适的仿函数类传给HashTable的模板参数Hash了。然后要改的是:在Insert、Find、Erase函数中,都把用于生成仿函数对象hf的语句从hashfunc hf;改成Hash hf。

除此之外,在Insert函数中,还需要将语句int hashaddr = hf(cur1->_data) % v1.size();改成int hashaddr = hf(cur1->_data.first) % v1.size();、把语句int hashaddr = hf(x) % _v.size();改成int hashaddr = hf(x.first) % _v.size();。为什么呢?因为结合上一段可知hashfunc不支持转化pair类1型,只支持转化pair的first成员的类型K,而cur1->_data和x刚好都是pair类型,所以需要修改代码。

4,在Find函数中把语句if (cur->_data == x)改成if (cur->_data.first == x),然后在Erase函数中,将语句if (cur2->_data == x)改成if (cur2->_data.first == x),原因和上一段相同。

5,在用于检测哈希表的函数print中,把语句从cout << cur->_data<<“  ”;改成cout << cur->_data.first << ':' << cur->_data.second << "  ";。因为我们知道通过HashTable封装unordered系列的关联式容器时,T一定是pair,而哈希表中节点类Node的表示数据的成员T _data也就是pair类型的数据了,pair类型的数据是无法通过cout<<直接打印的,因此需要修改代码。

6,在<<哈希的介绍以及哈希表的模拟实现>>一文中说过,该篇文章中实现的哈希表的Insert函数并没有编写去重逻辑,所以这里为了封装unordered_map,要将去重逻辑补上,思路也很简单,在Insert函数中调用Find函数,如果找到了目标元素,就不插入,如果没找到,则就继续插入。

————分割线————

在上文中说过,unordered系列的关联式容器的底层都是通过开散列的方式所实现的哈希表,因此unordered_map的迭代器完全等价于在<<哈希的介绍以及哈希表的模拟实现>>一文中的通过开散列的方式所实现的哈希表的迭代器。这里也需要对哈希表的迭代器类的代码做一些修改,哪些修改呢?

(以下内容建议结合<<哈希的介绍以及哈希表的模拟实现>>一文中的通过开散列的方式所实现的哈希表的迭代器进行理解、比对)

1,首先要给类模板HashIterator在原有的基础上增加一个模板参数class K来表示pair的first成员的类型。原因是类模板HashIterator中有HashTable类的成员对象,而从上文中可知HashTable是刚增加了模板参数K的,所以这里的原因1就是要把这个K传给HashIterator类模板中的HashTable类,以让HashTable类能成功的实例化。原因2是因为在HashIterator类模板的成员函数operator++中,我们需要哈希函数通过节点类的成员T _data计算节点所处的桶的位置(也就是哈希地址),因此需要仿函数类class Hash = hashfunc来将任意类型的_data转化成size_t类型的值,而我们知道通过HashTable封装unordered系列的关联式容器时,T一定是pair,而当T是pair类型时,我们希望通过pair的first成员来将pair转化成size_t类型的值,因此这就是需要给类模板HashIterator在原有的基础上增加一个模板参数class K来表示pair的first成员的类型的第二个原因。

2,从上一段可知,需要在定义类模板HashIterator的template>处,把hashfunc改成hashfunc。然后在前后置的operator++函数中,把语句int hashaddr = hf(_n->_data) % _ht->_v.size();改成int hashaddr = hf(_n->_data.first) % _ht->_v.size();,因为T类型的_data的中,T是pair类型,而上一段说过当T是pair类型时,我们希望通过pair的first成员来将pair转化成size_t类型的值。

3,从上上段中可知,还需要将类模板HashIterator的成员变量const HashTable* _ht改成const HashTable* _ht;。然后把重命名的地方做修改,从typedef HashTable HT;改成typedef HashTable HT;。

4,以上就是HashIterator迭代器类中需要进行的修改,最后只需要在HashTable类中把HashIterator的友元声明和重命名HashIterator的地方做修改,毕竟HashIterator是增加了模板参数class K来表示pair的first成员的类型的。因此需要在HashTable类中把friend class HashIterator;改成friend class HashIterator;,把friend class HashIterator;改成friend class HashIterator;,把typedef HashIterator iterator;改成typedef HashIterator iterator;,把typedef HashIterator const_iterator;改成typedef HashIterator const_iterator;

5,迭代器类整体修改完毕后,在哈希表类的Insert函数中,把该函数的返回值类型从bool改成pair,并把return的地方从true、false也改成对应的pair。最后再把哈希表类的Find函数的返回值类型从Node*改成iterator,再把return的地方从Node*类型的值换成iterator类型的值(注意如果没找到,则返回iterator(nullptr,this),也就是返回和【end()函数返回的迭代器】指向相同的迭代器)

经过修改过后的开散列版本的HashTable的整体代码

根据上面的步骤,经过修改后的通过开散列的方式实现的哈希表OpenHash::HashTable和哈希表的迭代器OpenHash::HashIterator的代码如下。(以下是整个Hash.h的代码)

#pragma once
#include
#include
#include
using namespace std;


/*
	注意,该版本下的通过开散列的方式所实现的哈希表不是纯粹的哈希表,而是经过修改过的、用于封装unordered系列的关联式容器的哈希表
*/

namespace OpenHash//表示开散列的意思
{
	template
	struct hashfunc
	{
		size_t operator()(const T& x)
		{
			return (size_t)x;//如果T是指针类型,或者是char类型,int类型,double类型,float类型等能直接强转成size_t类型的类型,则通过该函数直接转即可
		}
	};

	template<>
	struct hashfunc
	{
		//BKDR字符串哈希方法
		size_t operator()(const string& s)
		{
			size_t val = 0;
			for (string::const_iterator it = s.begin(); it != s.end(); it++)
			{
				val *= 131;
				val += (*it);
			}
			return val;
		}
	};

	template
	struct HashNode
	{
		HashNode()
			:_data()
			, _next()
		{}

		HashNode(const T& x, HashNode* p = nullptr)
			:_data(x)
			, _next(p)
		{}

		T _data;
		HashNode* _next;
	};

	//HashTable类模板的定义在HashIterator的定义的下方,而HashIterator类内有HashTable的成员对象,因此这里需要前置声明,如果对前置声明不太熟悉,请看<<模板的进阶(包括模板的分离编译问题、前置声明问题)>>一文
	template
	class HashTable;

	//因为存在const_iterator这种东西,于是设置T2专门用于控制operator*和operator->的返回值;而T1就用于控制哈希表中节点Node的类型了
	//因为在operator++中有需要用到计算哈希地址的哈希函数,所以需要Hash这个仿函数类去将任意类型转化成size_t类型,辅助哈希函数计算哈希地址
	template>
	class HashIterator
	{
		typedef HashNode Node;
		typedef HashIterator iterator;
		typedef HashTable HT;
	public:
		HashIterator()
			:_n(nullptr)
			, _ht(nullptr)
		{}

		HashIterator(Node* p, const HT* ht)//注意这里的ht,和HashIterator类的指针成员_ht,都必须加上const,否则哈希表的成员函数const_iterator begin()const就编不过,原因是const_iterator构造不出来
			:_n(p)
			, _ht(ht)
		{}

		T2& operator*()
		{
			assert(_n != nullptr);
			return _n->_data;
		}

		T2* operator->()
		{
			assert(_n != nullptr);
			return &(_n->_data);
		}

		//后置++
		iterator operator++(int)
		{
			if (_n != nullptr)
			{
				Node* temp = _n;

				if (_n->_next != nullptr)
				{
					_n = _n->_next;
					return iterator(temp, _ht);
				}
				else
				{
					Hash hf;
					int hashaddr = hf(_n->_data.first) % _ht->_v.size();//已经把迭代器类设置成哈希表的友元类了,因此可以访问哈希表的private成员_v	
					hashaddr++;
					while (hashaddr < _ht->_v.size())
					{
						if (_ht->_v[hashaddr] == nullptr)
							hashaddr++;
						else
						{
							_n = _ht->_v[hashaddr];
							return iterator(temp, _ht);
						}
					}
					//走到这里,说明出了循环,说明所有桶都已经找完了,目前迭代器就指向最后一个元素,后面没有元素了
					_n = nullptr;
					return iterator(temp, _ht);
				}
			}
			else
			{
				return iterator(nullptr, _ht);
			}

		}

		//前置++
		iterator& operator++()
		{
			if (_n != nullptr)
			{
				Node* temp = _n;

				if (_n->_next != nullptr)
				{
					_n = _n->_next;
					return *this;
				}
				else
				{
					Hash hf;
					int hashaddr = hf(_n->_data.first) % _ht->_v.size();//已经把迭代器类设置成哈希表的友元类了,因此可以访问哈希表的private成员_v	
					hashaddr++;
					while (hashaddr < _ht->_v.size())
					{
						if (_ht->_v[hashaddr] == nullptr)
							hashaddr++;
						else
						{
							_n = _ht->_v[hashaddr];
							return *this;
						}
					}
					//走到这里,说明出了循环,说明目前迭代器就指向最后一个元素,后面没有元素了,让_n等于空后返回*this即可。
					_n = nullptr;
					return *this;
				}
			}
			else
			{
				return *this;
			}
		}

		/*
			开散列的哈希表的迭代器是单项迭代器,不支持前后置的operator--,其原因是通过开散列方式实现的哈希表的哈希桶是单链表,不支持向桶(即链表)的上方寻找。
		*/

		bool operator==(iterator it)const
		{
			return _n == it._n;
		}

		bool operator!=(iterator it)const
		{
			return _n != it._n;
		}
	private:
		Node* _n;
		const HashTable* _ht;
	};

	template>
	class HashTable
	{
		friend class HashIterator;
		friend class HashIterator;
		typedef HashNode Node;
	public:
		typedef HashIterator iterator;
		typedef HashIterator const_iterator;
		HashTable()
			:_size(0)
			, _v()
		{}

		//HashNode是HashTable所new出来的,所以理应由HashTable去释放, 析构函数是在类的成员被销毁前调用的特殊成员函数
		~HashTable()
		{
			for (int i = 0; i < _v.size(); i++)
			{
				Node* cur = _v[i];
				while (cur != nullptr)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		//拷贝构造
		HashTable(const HashTable& ht)
			:_v(ht._v)
			, _size(ht._size)
		{
			for (int i = 0; i < ht._v.size(); i++)
			{
				if (ht._v[i] != nullptr)
				{
					_v[i] = new Node((ht._v[i])->_data);
					Node* cur1 = _v[i];//cur1指针用于操作正在构造的哈希桶中的HashNode
					Node* cur2 = ht._v[i];//cur2指针用于操作被拷贝的哈希桶中的HashNode

					cur2 = cur2->_next;
					while (cur2 != nullptr)
					{
						cur1->_next = new Node(cur2->_data);
						cur1 = cur1->_next;
						cur2 = cur2->_next;
					}
				}
			}
		}

		iterator begin()
		{
			int i = 0;
			while (i < _v.size())
			{
				if (_v[i] == nullptr)
					i++;
				else
					return iterator(_v[i], this);
			}
			//如果走出了循环,说明哈希表中一个元素也没有
			return iterator(nullptr, this);

		}

		const_iterator begin()const
		{
			int i = 0;
			while (i < _v.size())
			{
				if (_v[i] == nullptr)
					i++;
				else
					return const_iterator(_v[i], this);
			}
			//如果走出了循环,说明哈希表中一个元素也没有
			return const_iterator(nullptr, this);

		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		const_iterator end()const
		{
			return const_iterator(nullptr, this);
		}

		pair Insert(const T& x)
		{
			iterator p = Find(x.first);
			if (p != iterator(nullptr, this))
				return make_pair(p, false);

			Hash hf;

			//一石二鸟,哈希表为空时走这里扩容;哈希表的负载因子达到1时也走这里扩容
			if (_size == _v.size())
			{
				int newSize = _v.size() == 0 ? 10 : 2 * (_v.size());
				vectorv1;
				v1.resize(newSize);
				//将旧空间_v的数据都移到新空间v1上
				for (int i = 0; i < _v.size(); i++)
				{
					if (_v[i] != nullptr)
					{
						Node* cur1 = _v[i];
						while (cur1 != nullptr)
						{
							int hashaddr = hf(cur1->_data.first) % v1.size();//hashaddr表示新vector上的下标,注意因为发生了扩容,所以哈希函数这里是模v1的size,而不是模_v的size
							Node* cur2 = v1[hashaddr];//cur2表示新vector上第hashaddr个链表的头节点的地址 
							Node* cur3 = cur1->_next;
							cur1->_next = cur2;
							v1[hashaddr] = cur1;
							cur1 = cur3;
						}
						_v[i] = nullptr;
					}
				}
				//走到这里就出了for循环,表明已经把数据都挪动完毕了,将新vector交给哈希表管理即可,出了最外层的if分支后,旧vector中的节点的内存不会被释放,释放的是旧vector中的指针所占的8字节空间
				_v.swap(v1);
			}
			//不管是否发生哈希冲突,插入元素x都需要执行以下逻辑
			int hashaddr = hf(x.first) % _v.size();
			Node* cur = _v[hashaddr];//cur是哈希表中下标为hashaddr位置上的链表
			Node* temp = new Node(x);//temp是需要新插入的节点
			temp->_next = cur;
			_v[hashaddr] = temp;
			_size++;
			return make_pair(iterator(temp, this), true);
		}

		iterator Find(const K& x)
		{
			Hash hf;

			//防止计算哈希地址时,int hashaddr = hf(x) % _v.size()的时候去模0
			if (_v.size() == 0)
				return iterator(nullptr,this);

			int hashaddr = hf(x) % _v.size();
			Node* cur = _v[hashaddr];
			while (cur != nullptr)
			{
				if (cur->_data.first == x)
				{
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return iterator(nullptr, this);
		}

		bool Erase(const K& x)
		{
			Hash hf;

			//防止计算哈希地址时,int hashaddr = hf(x) % _v.size()的时候去模0
			if (_v.size() == 0)
				return false;

			int hashaddr = hf(x) % _v.size();
			Node* cur1 = nullptr;
			Node* cur2 = _v[hashaddr];
			while (cur2 != nullptr)
			{
				if (cur2->_data.first == x)
				{
					if (cur1 != nullptr)
					{
						cur1->_next = cur2->_next;
						delete cur2;
						_size--;
						return true;
					}
					else
					{
						_v[hashaddr] = cur2->_next;
						delete cur2;
						_size--;
						return true;
					}
				}
				else
				{
					cur1 = cur2;
					cur2 = cur2->_next;
				}
			}
			//走到这里就出了循环,说明vector中的hashaddr号哈希桶上不存在目标元素,那哈希表中也就不存在目标元素,直接return false
			return false;
		}

		void print()
		{
			for (int i = 0; i < _v.size(); i++)
			{
				if (_v[i] != nullptr)
				{
					Node* cur = _v[i];
					cout << "哈希桶" << i << "为:";
					while (cur != nullptr)
					{
						cout << cur->_data.first << ':' << cur->_data.second << "  ";
						cur = cur->_next;
					}
					cout << endl;
				}
			}
			cout << endl;
		}

		//计算哈希表中有多少个节点(即有效数据),或者说计算vector的所有哈希桶中的节点个数之和
		//用于测试性能
		size_t size()const
		{
			return _size;
		}

		//计算哈希表的长度(即capacity),也就是计算vector的size
		//用于测试性能
		size_t CapacityOfHashTable()const
		{
			return _v.size();
		}

		//计算有多少个哈希桶上挂着节点
		//用于测试性能
		size_t HashBucketNum()const
		{
			size_t val = 0;
			for (int i = 0; i < _v.size(); i++)
			{
				if (_v[i] != nullptr)
				{
					val++;
				}
			}
			return val;
		}

		//计算vector中最长的哈希桶的长度
		//用于测试性能
		size_t MaxBucketLenth()const
		{
			size_t maxLenth = 0;
			for (int i = 0; i < _v.size(); i++)
			{
				Node* cur = _v[i];
				size_t lenth = 0;
				while (cur != nullptr)
				{
					lenth++;
					cur = cur->_next;
				}

				if (lenth > maxLenth)
				{
					maxLenth = lenth;
				}
			}
			return maxLenth;
		}


	private:
		vector_v;
		size_t _size;
	};

}

unordered_map的基础框架

在上文中说过,unordered系列的关联式容器的底层都是通过开散列的方式所实现的哈希表,unordered系列的关联式容器的迭代器就是开散列版本下的哈希表的迭代器,因此unordered_map的基础框架的代码如下。HashTable中已经存在了模板参数Hash,并且还有缺省值,但因为STL的标准中,unordered_map也有该模板参数,因此咱们也就为unordered_map加上了模板参数Hash。

#pragma once
#include"Hash.h"


template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	
private:
	OpenHash::HashTable,Hash>_ht;
};

unordered_map的迭代器、begin()、end()函数

有了上面修改过后的哈希表的迭代器的代码(即修改过后的HashIterator的代码),剩下的就简单了。unordered_map的迭代器完全等价于HashTable的迭代器,因此只需要typedef重命名哈希表的迭代器就生成了unordered_map的迭代器,因此unordered_map类的begin、end函数就是对HashTable类的begin、end函数的一层封装,如下所示。

说一下, 在<<哈希的介绍以及哈希表的模拟实现>>一文中说过,通过开散列的方式所实现的哈希表的迭代器是个单向迭代器,所以没有实现operator--等函数。而上文中说过,开散列版本下的哈希表的迭代器完全等价于unordered_map的迭代器,因此unordered_map的迭代器也是个单向迭代器。

#pragma once
#include"Hash.h"

template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	iterator begin()
	{
		return _ht.begin();
	}

	const_iterator begin()const
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator end()const
	{
		return _ht.end();
	}
private:
	OpenHash::HashTable, Hash>_ht;
};

unordered_map的Insert函数

unordered_map的Insert函数就是对HashTable的Insert的一层封装,因此unordered_map的Insert的代码如下。说一下,因为在上文中对<<哈希的介绍以及哈希表的模拟实现>>一文中的通过开散列的方式实现的哈希表OpenHash::HashTable进行过稍微的修改,修改过后的HashTable的Insert是实现了去重逻辑的。

#pragma once
#include"Hash.h"

template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	pair Insert(const pair& p)
	{
		return _ht.Insert(p);
	}
private:
	OpenHash::HashTable,Hash>_ht;
};

unordered_map的operator[]函数

unordered_map的operator[]函数需要复用哈希表的Insert函数,因为经过修改过的哈希表的Insert是完成了去重的功能的,所以如果哈希表中已经存在和要插入的pair对象A的first成员值相等的pair对象B了,那么不再插入A,返回一个pair对象C,first成员是指向pair对象B的迭代器,second成员是false;如果哈希表中不存在和要插入的pair对象A的first成员值相等的pair对象B,则插入A,返回一个pair对象C,first成员是指向pair对象A的迭代器,second成员是true。最后根据前面是哪一种情况,最后在unordered_map的operator【】函数中,return一个通过【解引用pair对象C的first成员(即迭代器)】得到的pair对象A或者B的second成员,也就是KV模型中的Value值的引用。

#pragma once
#include"Hash.h"

template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	V& operator[](const K& x)
	{
		pair p = _ht.Insert(make_pair(x, V()));
		return p.first->second;
	}
private:
	OpenHash::HashTable,Hash>_ht;
};

对以上接口的阶段性测试

因为unordered_map的operator[]复用了unordered_map的Insert,unordered_map的范围for又复用了unordered_map的begin()、end()和HashIterator的operator++,因此下图代码是间接检测了上文中unordered_map目前已经讲解的所有的接口,从运行结果可以看到是符合预期的。

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第5张图片

上图代码如下。

#include
using namespace std;
#include
#include"UnorderedMap.h"


void test1()
{
	UnorderedMapm;
	string s[9] = { "香蕉","苹果","香蕉","菠萝","苹果","香蕉","香蕉","菠萝","苹果" };
	for (string& e : s)
	{
		m[e]++;
	}
	for (const pair& p : m)
	{
		cout << p.first << ':' << p.second << endl;
	}
	cout << endl;
	cout << "——UnorderedMap的详细状态如下——"<

unordered_map的Erase函数

在上文中说过,unordered系列的关联式容器的底层都是通过开散列的方式所实现的哈希表,因此unordered_map的Erase函数就是对哈希表的Erase的一层封装,代码如下。

#pragma once
#include"Hash.h"


template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	void Erase(const K& x)
	{
		_ht.Erase(x);
	}
	
private:
	OpenHash::HashTable,Hash>_ht;
};

unordered_map的Find函数

在上文中说过,unordered系列的关联式容器的底层都是通过开散列的方式所实现的哈希表,因此unordered_map的Find函数就是对哈希表的Find的一层封装,代码如下。

#pragma once
#include"Hash.h"

template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	iterator Find(const K& x)
	{
		return _ht.Find(x);
	}

private:
	OpenHash::HashTable,Hash>_ht;
};

unordered_map的整体代码

以下是整个unordered_map.h的代码,注意Hash.h的整体代码在上文中标题为底层结构的部分。

#pragma once
#include"Hash.h"

template>
class UnorderedMap
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	pair Insert(const pair& p)
	{
		return _ht.Insert(p);
	}

	void Erase(const K& x)
	{
		_ht.Erase(x);
	}

	iterator Find(const K& x)
	{
		return _ht.Find(x);
	}

    //该接口在STL标准中不存在,这里存在只是为了测试unordered_map的各个模块的代码的是否存在问题
	void print()
	{
		_ht.print();
	}
	
	iterator begin()
	{
		return _ht.begin();
	}

	const_iterator begin()const
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator end()const
	{
		return _ht.end();
	}

	V& operator[](const K& x)
	{
		pair p = _ht.Insert(make_pair(x, V()));
		return p.first->second;
	}
private:
	OpenHash::HashTable,Hash>_ht;
};

unordered_set的模拟实现

unordered_set的整体代码

对比unordered_map的模拟实现,unordered_set只有一小部分地方存在区别,因此对于unordered_set的模拟实现,我们直接在unordered_map的代码上稍作修改即可。哪些地方需要修改呢?

1,只要是关联式容器,那么底层结构都是哈希表,而哈希表的哈希桶(即链表)中的节点Node的数据_data成员一定都是一个个pair,比如在unordered_map里是pair,而unordered_set里则是pair,因此需要在定义unordered_set的template<,,,>处,把class V给删除。

2,然后把unordered_set的Insert函数的参数作修改,从const pair& p 改成 const K& x,然后在Insert函数中把语句return _ht.Insert(p);改成return _ht.Insert(make_pair(x,x));,因为STL标准的unordered_set的Insert函数只需K类型的参数,不能用pair类型的参数。

3,最后把类模板unordered_set的私有成员变量OpenHash::HashTable, Hash>_ht;改成OpenHash::HashTable, Hash>_ht;

在unordered_map的整体代码的基础上,经过上面的步骤进行修改后,unordered_set的整体代码也就生成了,代码如下。

(注意Hash.h的整体代码是不用进行任意修改的,并且Hash.h的代码在上文中标题为底层结构的部分)

#pragma once
#include"Hash.h"

template>
class UnorderedSet
{
public:
	typedef typename OpenHash::HashTable, Hash>::iterator iterator;
	typedef typename OpenHash::HashTable, Hash>::const_iterator const_iterator;

	pair Insert(const K& x)
	{
		return _ht.Insert(make_pair(x,x));
	}

	void Erase(const K& x)
	{
		_ht.Erase(x);
	}

	iterator Find(const K& x)
	{
		return _ht.Find(x);
	}

	//该接口在STL标准中不存在,这里存在只是为了测试unordered_map的各个模块的代码的是否存在问题
	void print()
	{
		_ht.print();
	}

	iterator begin()
	{
		return _ht.begin();
	}

	const_iterator begin()const
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator end()const
	{
		return _ht.end();
	}

private:
	OpenHash::HashTable, Hash>_ht;
};

常见面试题:一个类型K去做set和unorder set的模板参数有什么要求?

set

set要求K类至少支持小于比较函数operator<或者operator>中的一个(当然两个都支持则更好),或者显示提供比较K类对象的仿函数。因为set的底层是红黑树,红黑树又是搜索树,在插入或者删除时需要向左向右遍历找,比如目标节点小于当前节点就向左找,反之向右找。

为什么最少可以只需要operator<或者operator>中的一个呢?因为假如说只有operator<,那 if (cur和operator==。

unordered_set

        1:要求K类型对象可以转换成整形、或者提供转成整形的仿函数。因为通过哈希函数和节点Node的成员数据T _data计算某个节点Node的哈希地址时,首先需要确保T类能转化成size_t类型,否则无法继续计算哈希地址。

        2:要求K类至少可以支持等于比较函数operator==、或者提供等于比较的仿函数。因为对于unordered_set而言,找一个节点的方式和set不一样,不是看比当前节点小还是大然后向左向右找,而是需要根据哈希函数计算哈希地址HashAddr,然后在vector的第HashAddr号哈希桶上遍历寻找目标节点,此时就需要operator==来确认当前节点是不是要找的目标节点。

unordered_multimap和unordered_multiset的模拟实现

这边笔者就不写代码了,直接说思路。实现方式也很简单,首先在上文中标题为底层结构的部分的Hash.h的代码中、把Hash.h中的HashTable类模板的Insert函数的去重逻辑给删除,方式为把下图红框处的代码给删除即可。

unordered系列的map\multimap\set\multiset的介绍与模拟实现_第6张图片

剩下的就非常简单了,比如说unordered_multimap的整体代码就是在unordered_map的整体代码的基础上,把类模板的名字从unordered_map改成unordered_multimap,于是就生成了unordered_multimap的整体代码;

然后unordered_multiset的整体代码就是在unordered_set的整体代码的基础上,把类模板的名字从unordered_set改成unordered_multiset,于是就生成了unordered_multiset的整体代码。

当然这样做只是实现了unordered_multimap和unordered_multiset的整体代码的大致框架,算是简略版本,有些细节和成员函数笔者是没有实现的。

你可能感兴趣的:(STL中容器的介绍与模拟实现,数据结构,c++)