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

news2024/11/28 6:36:45

文章目录

  • 前言
  • 一、unordered_map/unordered_set容器
    • (1)unordered_map容器介绍及使用
    • (2)unordered_set容器介绍及使用
    • (3)它们和map/set对比
  • 二、容器底层结构
    • (1)哈希表概念
    • (2)哈希冲突
    • (3)解决哈希冲突
  • 三、闭散列模拟实现
    • (1)节点
    • (2)插入
    • (3)删除
    • (4)查找
  • 四、开散列模拟实现
    • (1)节点
    • (2)插入
    • (3)查找
    • (4)删除
    • (5)完善
  • 五、源代码
    • (1)闭散列源代码
    • (2)开散列源代码

前言

前面讲了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为存储元素底层空间总的大小。如下图:
在这里插入图片描述
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…),但是二次探测还是不能解决踩踏。
如果插入的时候,先通过哈希函数获取插入新元素在哈希表中的位置,然后如果该位置中没有元素,就插入新元素,反之发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。如图:
在这里插入图片描述

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

开散列:
开散列法又叫链地址法或开链法,这是解决哈希冲突的最优方法,首先对key集合用散列函数计算散列地址,具有相同地址的key归于同一子集合,每一个子集合叫做一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。如下图:
在这里插入图片描述

三、闭散列模拟实现

我们先要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)。如图:
在这里插入图片描述
另外哈希表增删查改的平均时间复杂度是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,如图:
在这里插入图片描述
所以极端情况很少出现,有一种方法可以解决单个桶超过一定长度,这个桶可以改成红黑树。

二、接下来是完善的是:对模的这个数进行优化
有些书上建议这个模的这个数也就是表的大小,最好是素数,它只能被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

#include"OpenHash.h"

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

}

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

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

相关文章

分布式算法之一致性 Hash 算法

tip&#xff1a;作为程序员一定学习编程之道&#xff0c;一定要对代码的编写有追求&#xff0c;不能实现就完事了。我们应该让自己写的代码更加优雅&#xff0c;即使这会费时费力。 &#x1f495;&#x1f495; 推荐&#xff1a;体系化学习Java&#xff08;Java面试专题&#…

第35步 机器学习实战DLC:不平衡数据处理(下)

失踪人口回归的第二期&#xff0c;继续说一说用PSM处理不平衡数据。 一、啥叫PSM PSM全称为Propensity Score Matching&#xff0c;翻译过来就是倾向匹配得分&#xff0c;为了省流&#xff0c;让小Chart介绍一下&#xff1a; 放到我们的数据就是&#xff1a;根据某个特征&…

架构师需要看透公司的用户心智定位

一个架构师要站在用户的角度去思考架构的规划和设计。把注意力放在用户身上&#xff0c;已经有无数案例证明是可以带来重大商业和技术突破的。 很多创业公司&#xff0c;从初创到倒闭&#xff0c;都没搞清楚自己的目标人群和心智。如果一个公司&#xff0c;能锁定目标人群及其…

7个最佳WooCommerce跨境电商社交媒体插件

在互联的早期&#xff0c;您通常可以将产品直接放到网上并出售。但在今天你必须进行营销和做广告。如果没有包括社交媒体在内的可持续营销策略&#xff0c;您的商店可能会被忽视。值得高兴的是&#xff0c;有很多 WooCommerce跨境电商社交媒体插件可以用来传播信息&#xff0c;…

【CMake 入门与进阶(5)】 CMakeLists.txt 语法规则基础及部分常用指令-续(附使用代码)

project project命令用于设置工程名称&#xff1a; # 设置工程名称为 HELLO project(HELLO)执行这个之后会引入两个变量&#xff1a;HELLO_SOURCE_DIR 和 HELLO_BINARY_DIR&#xff0c;注意这两个变量名的前缀就是工程名称&#xff0c;HELLO_SOURCE_DIR 变量指的是 HELLO 工程…

华为推出首款全栈自主数据库 GaussDB,你怎么看?

鸿蒙套壳&#xff0c;鸿蒙套壳完了ERP套&#xff0c;ERP套壳&#xff0c;ERP套壳完了数据库套壳&#xff0c;数据库套壳完了…… 犹记得GaussDB之前一直宣传是基于PostgreSQL研发而来&#xff0c;不知道今天为啥摇身一变为首款全栈自主分布式数据库了。 基于开源研发改不恶心。…

智能设备管理系统

传统设备实施管理难点&#xff1a; 1、日常工作繁琐&#xff0c;手动纸质记录和 Excel 管理设备数据麻烦&#xff0c;后期难以汇总管理&#xff0c;且数据易丢失。 2、需核对设备巡检、保养、故障维修记录&#xff0c;手动更新设备状态和最近维修时间等。 3、无法实时获取设备最…

大学生网络工程想走网络安全方向该怎么规划?

明确需求,确定方向 网络安全 网络安全 是一个很广的概念&#xff0c;涉及的岗位也是非常多的&#xff0c;有安全服务、安全运维、渗透测试、web安全、安全开发、安全售前等等。可以看看下面每个岗位的要求与自身兴趣能力匹配度再决定最适合自己的方向。 渗透测试/Web安全工程师…

开启人机协作新时代:协作机器人的应用与展望

原创 | 文 BFT机器人 01 蓄势待发&#xff0c;产业变革新引擎 近年来&#xff0c;在政策扶持、资本助推和技术创新的共同作用下&#xff0c;产业迎来发展黄金期。日前&#xff0c;各行各业正经历产业智能化转型&#xff0c;机器人市场规模不断扩大&#xff0c;发展前景广阔&…

万宾建筑结构健康监测系统方案

建筑结构健康监测是现代建筑工程领域的重要措施之一。通过实时监测和评估建筑物的结构状态&#xff0c;可以及早发现潜在的问题&#xff0c;保障建筑物的安全性和稳定性。 随着城市化进程的加速和建筑规模的扩大&#xff0c;建筑结构的安全性和稳定性越来越受到关注。然而&…

chatgpt赋能python:Python快速打开:如何提高Python执行速度

Python 快速打开&#xff1a;如何提高 Python 执行速度 介绍 Python 是一种解释型语言&#xff0c;由于其简洁易读&#xff0c;广泛用于数据科学、机器学习、Web 开发等领域。然而&#xff0c;它的执行速度相对较慢&#xff0c;这通常是由于其解释器中面临的硬件资源限制以及…

如何查看docker下的mysql版本

进入运行的mysql的容器 docker exec -it mysqlserver bash 标红的位置可以是我们运行的别名&#xff0c;也可以为id 我们想连接mysql服务时报错了 我们看下配置文件 cat /etc/my.cnf 可以看到配置文件的sock文件位置并不在/var/lib/mysql文件夹中 这里又两种办法 1.直接修改…

JVM--方法区元空间

前言 本篇对java的JVM线程共享内存中的方法区进行系统性的讲解。 1、方法区&元空间概念 方法区是《Java虚拟机规范》中规定的一个内存区域&#xff0c;它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。 元空间是方法区的实现。方法…

python基础----09-----类、对象、魔法方法、封装、继承、类型注解、多态、数据分析案例

一 初识对象 说白了就是类的实例化&#xff0c;类是一个抽象层的定义。 例如下面class Student就是定义的一个类&#xff0c;它是抽象层&#xff0c;然后stu_1 Student()&#xff0c;我们根据类创建了一个对象&#xff0c;就是对类的实例化&#xff0c;这个实例化对象我们是可…

paas云底座-数字化转型,你真的了解么

云底座是什么&#xff1f; 如图 底座其实就是一个基础打个比方&#xff1a; 把底座看成一块土地&#xff0c;我们在这块土地上可以盖楼房&#xff0c;可以挖一个游泳池&#xff0c;可以开一家饭店 也就是说我们这块土地可以开发很多东西言归正传 云底座其实就是数字化转型的“底…

【快速幂】-迭代法:详解

何为快速幂&#xff1f; 我们经常会计算&#xff1a;。STL中有自带的pow函数&#xff0c;如果当n很大的时候&#xff0c;那么一定会TLE。 因此&#xff0c;我们需要另一种求值的方法&#xff1a;快速幂&#xff01; 快速幂有两种做法&#xff1a;1&#xff1a;递归 2…

谷歌云 | 宣布跨云互连:无缝连接到您的所有云

【本文由Cloud Ace整理发布&#xff0c;Cloud Ace 是谷歌云全球战略合作伙伴&#xff0c;拥有 300 多名工程师&#xff0c;也是谷歌最高级别合作伙伴&#xff0c;多次获得 Google Cloud 合作伙伴奖。作为谷歌托管服务商&#xff0c;我们提供谷歌云、谷歌地图、谷歌办公套件、谷…

1 软件测试基本概念

文章目录 课程目标1. 入门前的7个基础问题2. 软件测试基本概念2.1 需求的概念2.1.1需求的基本概念2.1.2 从软件测试人员角度看需求2.1.3 为什么需求对软件测试人员如此重要&#xff1f;2.1.4 如何才可以深入理解被测试软件的需求 2.2 bug的概念(了解)2.3测试用例的概念2.3.1 概…

AI读心术

近期&#xff0c;德克萨斯大学奥斯汀分校的神经科学家们展开了一场「AI」实验&#xff0c;利用人工智能聊天机器人ChatGPT&#xff0c;将大脑活动转化为文字信息。 参与实验的志愿者&#xff0c;在进行长达20小时的「训练」后&#xff0c;成功被「AI」识别出正在进行的活动。按…

portraiture宿主插件最新v4中文版本下载及使用教程

自拍怎么可以不修图呢&#xff1f;如果要修图的话&#xff0c;磨皮就是其中非常重要的一环。皮肤看起来细腻光滑了&#xff0c;整个人的颜值都会瞬间拉高。下面就让我们介绍一下磨皮用什么软件好用&#xff0c;什么软件可以手动磨皮的相关内容。portraiture是ps人像修图中常用的…