C++哈希

本文介绍了C++11引入的unordered系列关联式容器,如unordered_map和unordered_set,强调了它们底层使用哈希结构以提供高效查找的优势。文章详细阐述了哈希的概念、冲突解决方法(闭散列与开散列)并提供了代码实现,讨论了哈希冲突的解决策略,包括线性探测和二次探测。同时,探讨了仿函数在哈希中的应用,以及如何处理自定义类型的哈希和比较。最后,文章展示了如何封装unordered_map和unordered_set,并介绍了迭代器的操作,包括解引用、前置自增、构造、begin()、end()和operator[]的实现。
摘要由CSDN通过智能技术生成

目录

unordered系列关联式容器

unordered_map

unordered_map的文档介绍

unordered_set

底层结构

哈希概念

哈希冲突的解决

闭散列:

代码实现

哈希基本结构

插入

查找find

删除

仿函数的应用

开散列

基本结构

插入的实现

查找

删除

Unordered_set/Unordered_set对哈希的封装

封装代码框架:

哈希表的迭代器

【operator*和operator->】

operator++前置++

 迭代器的构造

begin()和end()

operator!=

operator[]

哈希表的拷贝构造和赋值重载和析构

拷贝构造

赋值重载

析构


unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 ,即最差情况下 需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次 数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑 树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和 unordered_set进行介绍,unordered_multimap和unordered_multiset的用法可以类比于multimap和multiset。

unordered_map

unordered_map的文档介绍

  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的 value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键 和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内找到key所 对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率 较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
  6. 它的迭代器至少是前向迭代器 (forward iterators

unordered_set

在线文档说明

底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经 过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( logN)(以2为底),搜索的效率取决 于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过 某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函 数可以很快找到该元素。

那么当存储一组数据时候,比如开辟了一组256空间的int数组,int arr[256],我们按照下标的位置来存储数据,0就存储在0的位置,256就存储在256的位置,这样会造成,有大量的空间没有被使用而造成空间的浪费。为了避免这个情况,我们可以使用哈希,当开辟了10个数组的空间,我们将key值模上数组的大小,这样数组存放的下标就不会超过10,使得256就存储在6的位置,使空间减小,并且搜索效率提高。

那么又有一个新的问题,当数据多了(在不超过数组大小的情况下),会造成冲突,比如2,12,22,32这组数据,在数组空间大小为10的时候,都会放在2这个位置,造成了哈希冲突。

哈希冲突的解决

闭散列:

也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那 么可以把key存放到冲突位置中的“下一个” 空位置中去。

1、线性探测

插入:

用除留余数法求出key值的关键码,并将它放到对应的位置上。如果该位置已经存在数据被占用了,那么继续寻找下一个位置,也就是+1的位置,如果+1的位置已经有数据,那么继续+1。

查找:

当查找哈希中的数据,在除留余数后依次找到对应位置,查找的条件是数组对应的位置不为空,但是如果某一个位置的值删除,再次查找一个数据,可能本来存在的,却因为中途遇到空而停止,找不到该数据。比如2,12,22,32这组数据,第一次按序排列,能够找到,删除12后,再次从2开始查找,遇到了空停下来,找不到了。

所以第一种解决方法,是在删除后将后面的数填到该删除的位置,依次的填入,但是这样很麻烦,所以第二种方法:我们用一个枚举,将数组中每个数据的状态记录一下,所以就有了存在,空和删除这三种状态,当数据被删除了,状态变为删除。但是这个数据还没有从数组中的位置删除,这样数组该位置并不是空,不会因为数据被删除而停下来。并且没有挪动数据那么麻烦。

因为数据多了,很大概率会造成哈希碰撞,本来在原有的位置,但是因为线性探测,将此位置占用。连续位置值冲突比较多,会引发踩踏洪水效应。所以还有一种二次探测的方法。

2、二次探测

二次探测和线性探测差不多,唯一的不同就是线性探测是遇到冲突的位置就+1个位置,而二次探测会+i平方的位置,第一次冲突找1*1的位置,第二次冲突找2*2的位置,也就是+4的位置。这样使得挤在一起的数据,有些许的缓和。

我们来用代码实现一下:

代码实现

哈希基本结构

这里我们暂且用kv的结构来实现,后续如果要用map/set封装,我们之后再改。

我们将每个哈希数据都放在一个数组中。

	//枚举数据状态
    enum status
	{
		EXIST,
		EMPTY,
		DILTE
	};

    //哈希每个数据状态
	template<class K,class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _status = EMPTY;
	};

    //哈希表结构
	template<class K,class V>
	class HashTable
	{
	public:
	private:
		vector<HashData<K,V>> _tables;
		size_t _n;//有效数据个数
	};

插入

插入思路:插入的数据用除留余数法求出关键码,如果该码位置的状态是空,就插入数据;如果不是空,就继续线性探测找到空的位置。循环条件就是数组位置状态不是空。

线性探测和二次探测,下面代码中我们只要改变start+i的数据,就能控制,二次还是线性。

		bool Insert(const pair<K, V>& kv)
		{
			size_t start = kv.first % _tables.size();
			size_t i = 0;
			size_t index = start;
			while (_tables[index]._status == EMPTY)//如果位置被占用
			{
				i++;
				//index = start + i;//线性探测
				index = start + i^i;//二次探测
				index %= _tables.size();//防止加之后的数大于表格长度,模一下,无论多大的值,都在这个表格内。
			}

			_tables[index]._kv = kv;
			_tables[index]._status = EXIST;
			++_n;
		}

思考:哈希表什么情况下进行扩容?如何扩容?

也就是说扩容并不一定是数据满了才扩容,在负载因子大于0.7的时候,就需要扩容,这样同时也避免了更多的哈希冲突。

扩容的方法:

我们用一个负载因子来记录是否要扩容,如果负载因子大于0.7,那么就扩容。第一个方法是:开辟一个新的vector,将旧的_table的vector依次插入到新的vector中,但是这种方法,和插入的代码形成了冗余。所以我们用另一种方法:开辟一个新的哈希表HashTable<K, V> newHT,依次遍历旧的_table表,将_table表中的数据插入到哈希类的表中,此时插入新的哈希表,会复用Insert,newHT.Insert(_table[i]._kv),当再次使用Insert,因为newHT的size已经扩容到2倍了,所以不存在负载因子过大,这样就不会走扩容这个条件语句,进而直接走下面的插入代码。此时kv.first%_tables.size(),这里的kv是旧的_table[i]的kv数据,要模上newHT对象的_tables大小。这样就完成了Insert的复用。最后将旧的_tables和新的哈希表的_tables交换,最后走出这个栈帧,自动将旧的_talbes销毁,不用自己释放。

		bool Insert(const pair<K, V>& kv)
		{
			HashData<K, V>* ret = Find(kv.first);
			if (ret)
			{
				return false;
			}

			if (_tables.size()==0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newHT;
				newHT._tables.resize(newSize);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._status == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);
			}

			size_t start = kv.first % _tables.size();
			size_t i = 0;
			size_t index = start;
			while (_tables[index]._status != EMPTY)//如果位置被占用
			{
				i++;
				index = start + i;//线性探测
				//index = start + i^i;//二次探测
				index %= _tables.size();//防止加之后的数大于表格长度,模一下,无论多大的值,都在这个表格内。
			}

			_tables[index]._kv = kv;
			_tables[index]._status = EXIST;
			++_n;
			return true;
		}

查找find

在上面的代码还有一个问题,就是插入后,可能会插入重复的值,按上面的代码如果插入了重复的值,那么会插入到一个新的没有被占用的空间,也就是不等于空的空间。如果是del标识,那么它会覆盖。但是哈希不需要重复的值,所以我们用find函数来查找一下,是否有重复的值。

		HashData<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}

			size_t start = key%_tables.size();
			size_t i = 0;
			size_t index = start;
			while (_tables[index]._status != EMPTY)
			{
				if (_tables[index]._kv.first == key && _tables[index]._status==EXIST)
				{
					return &_tables[index];
				}

				++i;
				index = start + i;
				index %= _tables.size();
			}
			return nullptr;
		}

查找的思路和插入大致相似。如果不是空,那么一直查找,直到找到key这个数。直到遍历到空位置都找不到,那么就返回nullptr。用这个返回值类型,是为了方便在插入函数中,如果找到重复的数据,那么会返回该数据的地址,插入函数直接返回false;如果没有找到,就会返回nullptr,说明还没有插入过这个数字,插入函数继续继续向下执行。

这里需要注意,当第一次插入的时候,先执行find函数,因为find函数需要通kv.first和_tables.size()取模,此时第一次插入_tables的大小是0,会让程序崩溃。所以find函数最开始要加上一个判断条件,判断_tables.size()是不是0.是0的话返回nullptr

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值