C++【unordered_map/set的底层实现-哈希表】—含有源代码

这篇具有很好参考价值的文章主要介绍了C++【unordered_map/set的底层实现-哈希表】—含有源代码。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

前面讲了STL中的map和set容器以及封装实现,虽然它们的查找效率是O(logN),但是当红黑树中的节点非常多时,因为红黑树不是严格平衡,树的高度可能变得很大,就是一变高一边低的情况,这会导致查询操作的时间复杂度变为O(N),所以后面就出现了四个unordered系列的关联式容器,最常用的是unordered_map和unordered_set。

一、unordered_map/unordered_set容器

(1)unordered_map容器介绍及使用

unordered_map容器介绍:
它是以存储<key, value>键值对的方式去存储数据,可以高效地根据单个key值遭到对应的value。key是唯一的,kay和value的类型可以不相同。 另外,它存储元素是没有顺序的,它根据key的哈希值,相同哈希值的键值对放在相同的桶中,所以单个查找很高效,是在常数范围内,但它要查询某一范围内的key值在时要比map效率低,为什么?
因为它内部元素的排列顺序是不稳定的,这使得它的迭代器不能支持对元素子集进行稳定的顺序遍历,或者说它的迭代顺序是不可预测的,比如想遍历所有成绩等于某一值的学生,可以找到,因为它内部的哈希表不支持元素的稳定排序,因此找到的学生的顺序是不可预测的。
unordered_map也实现了直接访问操作符即operator[],它允许使用key作为参数直接访问value。而且key是不允许被修改,元素值可以被修改。
最重要的一点:它们的值是通过其键的相对位置来找,并不是绝对位置
接口使用:
构造:

构造一个空容器:
unordered_map<string, char> mp1;
拷贝构造一个容器:
unordered_map<string, char> mp2(mp1);
使用迭代器区间构造一个容器:
unordered_map<string, char> mp2(mp1.begin(), mp1.end());

接口函数:

bool empty() const:检测是否为空
size_t size() const:获取有效元素个数
begin:返回unordered_map第一个元素的迭代器位置
end:返回unordered_map最后一个元素下一个位置的迭代器
cbegin:返回unordered_map第一个元素的const迭代器
cend:返回unordered_map最后一个元素下一个位置的const迭代器
iterator find(const K& key):返回key在哈希桶中的位置
size_t count(const K& key) :返回哈希桶中关键码为key的键值对的个数
insert:向容器中插入键值对
erase:删除容器中的键值对
void clear :清空容器中的有效元素个数
void swap(unordered map&):交换两个容器中的元素

元素访问:

operator[ ] 返回与key对应的value,没有一个默认值

简单介绍一下[ ]的重载,该函数实际调用哈希桶的插入操作,用参数key与V()构造一个默认值向底层哈希桶中插入,后面的哈希桶在后面说。如果key不在哈希桶中,插入成功,返回V(),若key已经在哈希桶中,插入失败,将key对应的value返回。和map实现的[]一样。
接口使用:

begin:获取容器中第一个元素的正向迭代器
end:获取容器中最后一个元素下一个位置的正向迭代器
insert:插入指定元素
erase:删除指定元素
find:查找指定元素
size:获取容器中元素的个数
clear:清空容器
swap:交换两个容器中的数据
count:获取容器中指定元素值的元素个数

(2)unordered_set容器介绍及使用

unordered_set容器:
它不再以键值对方式去存储数据,变成了直接存储,没有重复元素,且不会对内部存储的数据进行排序,平均时间复杂度为常数。unordered_set中元素的值也是唯一标识它的键,键不可改变,所以它里面的元素在容器不能被修改。
同样:它们的值是通过其键的相对位置来找,并不是绝对位置

(3)它们和map/set对比

unordered_map和unordered_set以及map和set,都是不允许插入重复的元素的,当尝试插入一个已经存在于容器中的关键字或元素时,它们都不会插入成功。unordered_map和map可以允许插入多个元素值相同,但键(key)不同的元素。
unordered_map/unordered_set和map/set区别
1、实现不同:前者底层是用哈希表实现的,后者使用红黑树实现
2、性能不同:前者不按键值排序,插入时间复杂度是O(logN),查找时间复杂度是O(1),后者按键值排序,插入时间是Olog(logN),查找时间复杂度是O(logN)

二、容器底层结构

(1)哈希表概念

哈希背景:
前面也提到过,平衡树查找效率一般为O(logN),红黑树如果发生大两插入有可能退化为O(logN),而且顺序查找时间复杂度为O(N),这是因为元素关键码与存储位置之间没有对应关系,因此在查找一个元素时,必须要经过key的多次比较,搜索的效率取决于查找过程中元素的比较次数,现在要使查找效率更高效,找到了一种更优的结构,能一次找到要查找的元素并不经过任何比较,使复杂度为O(1),而这种结构叫哈希表。
哈希表概:通过某种函数使元素的存储位置与它的关键码之间能够建立一 一映射的关系,那在查找时通过该函数可以很快找到该元素(就像数组一样通过下标找到元素,只不过这里的下标是绝对位置)这种方法叫哈希方法,哈希方法中用的转换函数称为哈希函数或散列函数,构造出来的结构叫哈希表
哈希本质就上一种映射的算法思想,是一种转换。
unordered系列的关联式容器效率高的原因,是因为其底层使用了哈希结构,哈希表的关键字是通过哈希函数计算得到的,并且哈希函数是将关键字映射到一个桶中,桶中的元素通过链表或其他数据结构连接起来。
**哈希表举例:**例如:数据集合{11,17,16,14,15,19};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。如下图:
C++【unordered_map/set的底层实现-哈希表】—含有源代码
2种常见的哈希函数:
直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点简单、均匀,但去欸但需要事先知道关键字的分布情况,适合查找比较小且连续的情况。
2. 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。可以节省空间。

(2)哈希冲突

但是对于上面的哈希表,如果要存的元素是1,7就会发生冲突,它们映射到一同个位置,就发生冲突,这种冲突叫哈希冲突或哈希碰撞,含义是不同关键字通过相同哈希哈数计算出相同的哈希地址。

(3)解决哈希冲突

解决哈希冲突的方法有两种方法,分别为闭散列和开散列。
闭散列:
闭散列也叫开放定址法,当有哈希冲突时,如果哈希表未被填满,说明在哈希表中必然还有空位置,这样可以把key存放到冲突位置中的“下一个” 空位置中去,这里下一个带引号,表示并不是相邻的,因为相邻也可能有元素。
也就是说在自己的开放空间去找下一个位置,如果去找?
一种思路叫线性探测,这种方法很暴力,从当前位置往后去找,直到寻找到下一个空位置为止。但是一些相邻聚集位置连续冲突容易形成踩踏,踩踏就是访问别人的位置。可以这样减少踩踏:如果·哈希函数是hash=Key%len,计算出后+i(i>=1,1,2,3…)也可以+i^2(i>=1,1,2,3…),但是二次探测还是不能解决踩踏。
如果插入的时候,先通过哈希函数获取插入新元素在哈希表中的位置,然后如果该位置中没有元素,就插入新元素,反之发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。如图:
C++【unordered_map/set的底层实现-哈希表】—含有源代码

如果删除,在闭散列方法种不能随意删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素17,如果直接删除掉,7查找起来可能会受影响,所以线性探测采用标记的伪删除法来删除一个元素。比如在哈希表给每个空间做个标记,增加一种枚举类型表示三种状态:enum State{EMPTY, EXIST, DELETE};含义:EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除。

开散列:
开散列法又叫链地址法或开链法,这是解决哈希冲突的最优方法,首先对key集合用散列函数计算散列地址,具有相同地址的key归于同一子集合,每一个子集合叫做一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。如下图:
C++【unordered_map/set的底层实现-哈希表】—含有源代码

三、闭散列模拟实现

我们先要key和存储位置建立关联关系。我们采用线性探测就是依次找后面的位置存储。

(1)节点

#include<vector>
namespace nza
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};
	template<class K, class V>
	class HashTable
	{
	private:
		vector<HashData<K,V>>_tables;
		size_t _n;

	};

}

需要enum,每个存储位置的标识符,
再来个结点,存数据的节点和状态,转台初始化为空
我们用vector来存hash节点,这样也方便扩容。
_n 表示存储数据的个数

(2)插入

bool Insert(const pair<K, V>& kv)
		{
			if (find(kv.first))
				return false;

			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newt;
				newt._tables.resize(newsize);

					for (auto& data : _tables)
					{
						if (data._state == EXIST)
						{
							newt.Insert(data._kv);
						}
					}
					_tables.swap(newt._tables);

			}
			size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hash + i;
				index %= _tables.size();
				++i;
			}
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;
			return true;
		}

开始的newsize是我们要扩容多少,看是否等同于0,如果等于0先给个初始值多少都是可以,

先看最下面的线性探测:我先算起始位置hashi,即存储位置,用hash函数通过key去计算,这里我们要不能用capacity,因为这里会越界,[]会去检查下表小于size,上面说_n是有效数据个数,而_n之外我们不能去访问,所以要%size,然后用while循环判断,如果等于EXIST说明有元素占着,就找下一个位置,如果找出的大于有效数据个数就绕回去,直接取模,找到了就填充元素,并把状态更新为EXIST。
再处理两个问题讲解上面的代码:
1、如果size为0,会崩溃;
哈希表冲突也会受到影响,冲突越多,效率越低,如果都满了,再来一个值冲突的概率极大。这就需要载荷因子α,反映了表满的程度,他表示不是满了才扩容,它等于填入表的元素/表长度,α越大表示冲突的可能型越大。我们设置它在0.7到0.8,如果超过了我们标定的值就扩容,注意一点如果用reserve扩容扩容量capacity的话,size没变而且表为空,扩不了,所以我们先定义一个newsize表示我们要扩容多少,看是否等同于0,如果等于0先给个初始值多少都是可以,否则按2两倍扩。
2、如果表满了,需要扩容,但是size变了映射关系要变。
因为扩容,映射关系变了,原来冲突的值可能不冲突了,原来不冲突的值可能冲突了,这时候需要开一块新的空间,重新建立映射关系,重新定义一个newtables,让然后遍历旧表重新计算,最后交换资源和原来的_tables交换,但是这样太麻烦,代码大量冗余。可以这样解决:可以复用,创建一个哈希表对象HashTable<K, V> newt;,在类里面可以访问私有,把_tables扩容,再遍历旧表,直接插入,插入的时候复用了线性探测那段代码,最后交换资源。这是用新的哈希表对象再调用insert,因为空间开好了直接去走线性探测的代码,这里不是this调,不是直接的递归。
最终返回true,再开始的时候先查找一下key的值在不在,如果为空没有就返回false。

(3)删除

bool Erase(const K& key)
		{
			HashNode<K, V>* ret = find(key);
			if (ret)
			{
				ret->_state = DELETE;
				return true;
			}
			else
			{
				return false;
			}

		}

前面提到过需要标记一种状态,删除某一个位置,需要先通过key找到对应的存储位置去查找,第一次的时候可能不是,然后继续后找,找到后需要把设置为删除(delete)状态,因为查找是从映射位置开始找,直到空结束,如果删除了还是空会提前结束就不到了,所以我们如果成功查找才把它设置为delete状态并啊减减_n,然后返回真,否则返回假。

(4)查找

HashNode<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state != EMPTY)
			{
				if (_tables[index]._state==EXIST&&_tables[index]._kv.first == key)
				{
					return &_tables[index];
				}
				index = hashi + i;
				index %= _tables.size();
				++i;
				if (index == hashi)
				{
					break;
				}
			}
			return nullptr;
		}

为了防止除0错误先判断一下有没有有效数据,如果为0直接返回空,接着先计算key的存储位置,如果这个位置不为空,那么就进while循环找,如果找到了返回节点的地址,但是这里还要考虑删除,因为如果之前删除一个数,只是把这个位置标记为DELETE,它还是在的,如果下次去找的时候它还是返回我们已经删除的那个地址,所以if判断里面要加一个状态是不是存在,存在且key找到了然后再返回节点地址,但是如果里面的状态是删除+存在,这样就会在while循环里面一直循环,所以在下面再加一个判断,如果index等于hashi说明走完了一圈没有找到,就break停止。

四、开散列模拟实现

在实践过程种,我们不太期望上面闭散列方式,不管是线性探测还是二次探测都不是很好,我们更喜欢用拉链法或哈希桶实现哈希表。
上面提到拉链法就是把冲突的元素使用链式结构把它们挂起来。

(1)节点

template<class K,class V>
	struct HashNode
	{
		pair<K, V>_kv;
		HashNode<K, V>* _next;
		HashNode(const pair<K,V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{}
	};
	template<class K,class V>
	class HashTable
	{
	public:
	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

一样需要搞个节点,我们不用vecotr加list有些扩容细节不方便,我们用一个节点指针链接下一个节点和键值对存数据。哈希表类的私有成员用vector存节点指针,_n表示有效数据个数。相当于vector挂的是一个链表,里面存的是一个节点指针,有节点就挂起来,没有就是空。

(2)插入

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

			if (_n == _tables.size())
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newtables(newsize,nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;;
						size_t hashi = cur->_kv.first % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtables);
			}
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

先看最下面插入:
还是先计算存储位置hashi,然后我们头插,就是创建一个新节点,然后链接第一个节点,再把新插入的置为第一个节点。再更新_n。
同样也面两个问题:
我们还是要控制负载因子,负载因子越大,冲突概率越高,查找效率越低,空间利用率越高,这里设为1,也就是相等。扩容时和上面的逻辑一样,定义一个newsize表示我们要扩容多少,看是否等同于0,如果等于0先给个初始值多少都是可以,否则按2两倍扩。
因为扩容要解决重新映射的问题,在这里就不需要和上面一样定义一个哈希表对象再复用insert,因为还有优化的空间,因为上面又调了insert创建新节点,而且这里需要注意一点,把每个位置走完桶,过一会还要把原来的表释放掉,因为闭散列不需要写析构函数,它会自动释放,而这里的vecotr也会自动释放,但是不会把里面的指针释放,vector只是把空间释放了,自定义类型要调用自己的析构,开了新表,旧表还要释放。最好的方式是,还是用传统方法,把原表的节点重新计算位置挪动到新表,这里没有申请也没有释放。开一个指针数组,每个位置给空,然后进行挪动,头插需要保存下一个位置,然后重新计算新位置,进行头插并把它更新第一个位置,再把next给cur,继续往下走,遍历完之后旧表时,再交换资源,也就是地址的交换。

(3)查找

	Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
			cur = cur->_next;
			}
			return nullptr;
		}

查找先判断有效个数是否等于0,如果等于0直接返回空,再计算存储位置,定义一个指针变量,把刚计算的位置的节点指针赋给它,然后遍历没找到,继续更新节点,如果找到返回指针,否则返回空。

(4)删除

bool Erase(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;

					}
					delete cur;
					return true;

				}
				else
				{
					prev = cur;
					cur=cur->_next;
				}

			}
			return false;

		}

通过key计算存储位置,这就相当于单链表删除,需要知道它的前面的节点指针再定义一个当前指针,然后如果当前节点不为空进入while循环,如果找到了,再进行判断,如果前面前驱指针等于空,说明要删除的是第一个节点即头,直接把下一个位置赋给第一个节点,否则进行链接,然后再删除,返回真;如果while循环没结束且没找到,继续往下更新前驱指针和当前节点,结束没找到返回假。

(5)完善

一、对key进行完善优化

template<class K>
	struct HashFunc
	{
		size_t operator(const K& key)
		{
			return key;
		}

	};
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& str)
		{
			size_t hash = 0;
			for (auto& e : str)
			{
				hash += e;
				hash *= 33;
			}
			return hash;
		}

	};
	template<class K,class V,class Hash=HashFunc<K>>

如果key时字符串我们改怎么处理,这不能那样计算了,我们不能在代码写死,因为我们写的是一个泛型,我们可以再增加一个模板参数hash,支持一个仿函数,它就是可以把key转换成可以取模的整型,第一种就是本身就是能支持取模的,第二种是字符串里类型,我们需要把他转换成整形,为了减少和其他字符串的存储位置的冲突,我们可以对一个字符串的每一个字符进行先加再乘一个某一个数,据数据表明*31或着131…数字冲突的概率极低,这是字符串到整形的映射。另外我们为了不显示传,直接用了特化,自动匹配相应的类型。接下来在使用key取模的地方,创建一个Hash类型的对象hash,改成hash(key)。如图:
C++【unordered_map/set的底层实现-哈希表】—含有源代码
另外哈希表增删查改的平均时间复杂度是O(1),不看最坏,最坏的是多个数挂了在了相同的位置变成了O(N),因为有扩容的发生,几乎不会发生,有负载因子的控制如果是1,每个位置平均挂一个,但是没有那么理想,挂2到3个,看的是整体。
我们可以测试一下:
先写一个最大桶的长度函数

size_t MaxBuket()
		{
			size_t  Max= 0;
			for (int i = 0; i < _tables.size(); ++i)
			{
				auto cur = _tables[i];
				size_t count = 0;
				while (cur)
				{
					++count;
					cur = cur->_next;
				}
				if (count > max)
				{
					max = count;
				}
			}
			return max;
		}

结果我们进行随机测试,大多数情况都是0,1,而最大的是2,如图:
C++【unordered_map/set的底层实现-哈希表】—含有源代码
所以极端情况很少出现,有一种方法可以解决单个桶超过一定长度,这个桶可以改成红黑树。

二、接下来是完善的是:对模的这个数进行优化
有些书上建议这个模的这个数也就是表的大小,最好是素数,它只能被1或被自身整除,也是降低冲突的,我们直接看stl标准库的:提供一个素数表,存了28个素数,把当前的素数值传进去,在里面依次在表里遍历找比它大的值。

size_t GetNextPrime(size_t prime)
		{
			// SGI
			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 < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > prime)
					return __stl_prime_list[i];
			}

			return __stl_prime_list[i];
		}

五、源代码

(1)闭散列源代码

ClosedHash.h

#pragma once
#include<utility>
#include<vector>
using namespace std;

namespace nza
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V>
	class HashTable
	{
	public:
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newt;
				newt._tables.resize(newsize);

					for (auto& data : _tables)
					{
						if (data._state == EXIST)
						{
							newt.Insert(data._kv);
						}
					}
					_tables.swap(newt._tables);

			}
			size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();
				++i;
			}
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;
			return true;
		}
		HashNode<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state != EMPTY)
			{
				if (_tables[index]._state==EXIST&&_tables[index]._kv.first == key)
				{
					return &_tables[index];
				}
				index = hashi + i;
				index %= _tables.size();
				++i;
				if (index == hashi)
				{
					break;
				}
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			HashNode<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}

		}


	private:
		vector<HashNode<K, V>> _tables;
		size_t _n = 0; 
	};

	void Test()
	{
		int a[] = { 1, 2, 0, 4, 5, 20, 66 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}
		ht.Erase(3);
		ht.Insert(make_pair(80, 90));
	}
}

test.cpp

#include"ClosedHash.h"
int main()
{
	nza::Test();
}

(2)开散列源代码

OpenHash.h

#pragma once
#include<utility>
#include<vector>
#include<string>
#include<ctime>
#include<iostream>
using namespace std;

namespace nza
{
	template<class K,class V>
	struct HashNode
	{
		pair<K, V>_kv;
		HashNode<K, V>* _next;
		HashNode(const pair<K,V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{}
	};
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}

	};
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& str)
		{
			size_t hash = 0;
			for (auto& e : str)
			{
				hash += e;
				hash *= 33;
			}
			return hash;
		}

	};
	template<class K,class V,class Hash=HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			Hash hash;
			if (_n == _tables.size())
			{
				/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;*/
				size_t newsize = GetNextPrime(_tables.size());
				vector<Node*> newtables(newsize,nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;;
						size_t hashi = hash(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtables);
			}
			size_t hashi = hash(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}
		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
			cur = cur->_next;
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;

					}
					delete cur;
					return true;

				}
				else
				{
					prev = cur;
					cur=cur->_next;
				}

			}
			return false;

		}
		size_t MaxBucket()
		{
			size_t  Max= 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				auto cur = _tables[i];
				size_t count = 0;
				while (cur)
				{
					++count;
					cur = cur->_next;
				}
				printf("[%d]->%d\n", i, count);
				if (count > Max)
				{
					Max = count;
				}
			}
			return Max;
		}
		size_t GetNextPrime(size_t prime)
		{
			// SGI
			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 < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > prime)
					return __stl_prime_list[i];
			}

			return __stl_prime_list[i];
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};
	void Test()
	{
		int a[] = { 7, 5, 55, 13, 17, 12, 16 };
		HashTable<int, int> h1;
		for (auto e : a)
		{
			h1.Insert(make_pair(e, e));
		}
		h1.Insert(make_pair(22, 22));
		h1.Insert(make_pair(25, 25));
		h1.Insert(make_pair(36, 36));
		h1.Insert(make_pair(65, 65));
		h1.Erase(13);
	
			size_t N = 100000;
			HashTable<int, int> h2;
			srand(time(0));
			for (size_t i = 0; i < N; ++i)
			{
				size_t x = rand() + i;
				h2.Insert(make_pair(x, x));
			}
			cout << h2.MaxBucket() << endl;
	}

}

test.cpp文章来源地址https://www.toymoban.com/news/detail-477129.html

#include"OpenHash.h"

int main()
{
	nza::Test();
	return 0;

}

到了这里,关于C++【unordered_map/set的底层实现-哈希表】—含有源代码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【C++】开散列哈希表封装实现unordered_map和unordered_set

    【C++】开散列哈希表封装实现unordered_map和unordered_set

    在未达成目的之前,一切具有诱惑力的事物都显得那么不堪一击 1. 在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N l o g 2 ​ N ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。 最好的查询是

    2023年04月09日
    浏览(11)
  • C++进阶--使用哈希表实现unordered_map和unordered_set的原理与实例

    C++进阶--使用哈希表实现unordered_map和unordered_set的原理与实例

    本文将介绍如何使用哈希表来实现C++ STL库中的unordered_map和unordered_set容器。我们将会解释哈希表的基本原理,并给出具体的代码示例,帮助读者更好地理解和应用哈希表。 哈希原理讲解–链接入口 set和map的实现的文章,与unordered_map实现类似 unordered_set是一种集合存储的容器

    2024年04月09日
    浏览(13)
  • C++ 哈希+unordered_map+unordered_set+位图+布隆过滤器(深度剖析)

    C++ 哈希+unordered_map+unordered_set+位图+布隆过滤器(深度剖析)

    想象一下,你有一个巨大的图书馆,里面摆满了成千上万本书。如果你想要找到其中一本特定的书,你会怎么做?如果你按照书的编号来有序地排列它们,找到特定的书本可能会很容易。但是,如果书籍是随机地摆放,你可能需要逐本地查找,这个过程会非常耗时。 而哈希函

    2024年02月21日
    浏览(10)
  • C++利用开散列哈希表封装unordered_set,unordered_map

    C++利用开散列哈希表封装unordered_set,unordered_map

    1.之前我们已经实现了开散列的哈希表,今天我们来用它封装unordered_set,unordered_map 2.本文的封装比利用红黑树封装set和map更加复杂 建议大家先去看我的红黑树封装set和map再来看本文 因为有很多地方跟红黑树封装set和map时是同样的思路和方法,所以本文不会太去赘述一遍 因为un

    2024年03月24日
    浏览(10)
  • 【C++入门到精通】哈希 (STL) _ unordered_map _ unordered_set [ C++入门 ]

    【C++入门到精通】哈希 (STL) _ unordered_map _ unordered_set [ C++入门 ]

    欢迎各位大佬们的关顾,本文将介绍unordered系列容器以及其中的两个重要成员: unordered_map 和 unordered_set 。unordered_map是一种无序的关联容器,它使用哈希表来存储键值对,并提供高效的插入、查找和删除操作。在本文中,我们将首先介绍unordered_map的基本概念和特点,然后详

    2024年02月08日
    浏览(8)
  • 【C++】如何用一个哈希表同时封装出unordered_set与unordered_map

    【C++】如何用一个哈希表同时封装出unordered_set与unordered_map

    👀 樊梓慕: 个人主页  🎥 个人专栏: 《C语言》 《数据结构》 《蓝桥杯试题》 《LeetCode刷题笔记》 《实训项目》 《C++》 《Linux》 《算法》 🌝 每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.哈希桶源码  2.哈希表模板参数的控制 3.字符串作为键值如何映射哈希值

    2024年03月26日
    浏览(9)
  • 【C++】STL——用一个哈希表封装出unordered_map和unordered_set

    【C++】STL——用一个哈希表封装出unordered_map和unordered_set

    根据先前对unordered_map和unordered_set的学习,我们得知其底层是借助哈希表来实现的,接下来我们会使用上篇博客实现的开散列哈希表来模拟实现unordered_map和unordered_set,哈希表源代码链接: Hash/Hash/HashBucket.h · wei/cplusplus - 码云 - 开源中国 (gitee.com) 下面我们对哈希表进行改造,

    2023年04月18日
    浏览(15)
  • 从哈希桶角度看 unordered_map 与 unordered_set 的实现

    哈希函数与哈希桶是计算机科学中用于实现高效数据检索的重要工具。在之前的博客中,我们已经详细探讨了哈希的基本概念、哈希函数的构造方法以及哈希桶的工作原理等内容。在本篇博客中,我们将进一步深入探索C++中的unordered系列数据结构,并利用之前介绍的哈希桶原

    2024年03月22日
    浏览(11)
  • 由[哈希/散列]模拟实现[unordered_map/unordered_set] (手撕迭代器)

    由[哈希/散列]模拟实现[unordered_map/unordered_set] (手撕迭代器)

    以下两篇文章均为笔者的呕心沥血 想要搞懂本篇文章的uu请自行查阅 哈希/散列的细节实现 哈希/散列–哈希表[思想到结构][==修订版==] 手撕迭代器的细节处理 模拟实现map/set[改编红黑树实现map/set容器底层]

    2024年02月07日
    浏览(6)
  • 【C++进阶】使用一个哈希桶封装出unordered_map和uordered_set

    【C++进阶】使用一个哈希桶封装出unordered_map和uordered_set

    由于要使用一个哈希桶封装出unordered_map和uordered_set,所以要对哈希桶的模版参数进行改造,由于unordered_map是kv模型,而uordered_set是k模型,所以必须在实现哈希桶的时候提供模版,在封装uordered_set和unordered_map时传入不同的模版参数。 还要要修改的是,如果同时封装出,在获得

    2024年02月12日
    浏览(8)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包