哈希 || unordered系列的关联式容器底层 | 哈希模拟实现 | HashTable代码实现

news2025/1/24 22:45:24

底层结构

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

哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须经过关键码的多次比较

  • 顺序查找的时间复杂度为O\left ( N \right )
  • 平衡树中为树的高度,即O\left ( log_{}N\right )

搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素

如果构造一种存储结构,通过某种函数HashFunc使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找是通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素:
    • 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:
    • 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称为散列列表)。

哈希思路实例

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash ( key ) = key % capacity

capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。

但是这时就会有一些问题,按照上述的哈希方式,向集合插入元素44,会出现什么问题?

哈希冲突

对于两个数据元素的关键字,两个关键字不同a与b,但是Hash(a) == Hash(b) ,即:同关键字通过相同哈希函数计算出相同的哈希地址,该现象称为哈希冲突哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0——m-1之间;
  • 哈希函数计算出来的地址能均匀分布在整个空间中;
  • 哈希函数应该比较简单。

常见的哈希函数

直接定址法(常用)
  • 取关键字的某个线性函数为散列地址:Hash ( Key ) = A*Key + B
  • 优点:简单、均匀
  • 缺点:需要事先知道关键字的分布情况
  • 使用场景:适合查找比较小且连续的情况
除留余数法(常用)
  • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash ( Key ) = Key % p ( p <= m ),将关键码转换为哈希地址。
平方取中法(了解)
  • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
  • 关键字为4321,对它平方就是18671041,抽取中间的3位671(或者710)作为哈希地址。
  • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。
折叠法(了解)
  • 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
随机数法(了解)
  • 选择一个随机函数,取关键字的随机函数为它的哈希地址,即H(Key) = random(key),其中random为随机函数。
  • 通常应用于关键字长度不等时采用此法。
数学分析法(了解)
  • 设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现在频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
  • 数学分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。

  • 哈希函数设计的越精妙,产生哈希冲突的可能性就越低但;
  • 但是无法避免哈希冲突。

哈希冲突解决

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

闭散列

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

那如何寻找下一个空位置呢?

线性探测

比如,上述的示例,现在需要插入元素44,先通过哈希函数计算哈希地址,HashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  • 插入:
    • 通过哈希函数获取待插入元素在哈希表中的位置;
    • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到了下一个空位置,插入新元素。

  • 删除:
    • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来就可能会受到影响。因此线性探测采用标记的伪删除法来删除一个元素。
    • // 哈希表每个空间给个标记
      // EMPTY 空位置
      // EXIST 此位置已有元素
      // DELETE 元素已删除
      
      enum State
      {
          EMPTY,
          EXIST,
          DELETE
      };
    • 线性探测优点:实现非常简单。
    • 线性探测的缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

二次探测

线性探测的缺陷是产生冲突的数据堆积在一起,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:

  • Hash(i) = Hash(0) + i^2
  • Hash(i-1) = Hash(0) + (i-1)^2
  • Hash(i) = Hash(i-1) + (2i-1)

其中:i = 1,2,3……,Hash(0) 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置,m是表的大小。

对于上述的示例,如果要插入44,产生冲突,使用解决后的情况为:

 研究表明:

  • 当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。
  • 在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

 闭散列最大的缺陷就是空间利用效率比较低,这也是哈希的缺陷。

开散列

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

可以从下图中看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开散列与闭散列比较 

  • 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:
  • 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a<=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省了存储空间。

模拟实现

哈希表闭散列线性探测——open_address

open_address的HashTable的结构体以及待实现的函数

namespace openress {
	//枚举哈希数据的三种状态
	enum State
	{
		EXIST,
		DELETE,
		EMPTY
	};
	//定义一个存储键值对&&状态为空的数据
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable();
		bool Insert(const pair<K, V>& kv);
		HashData<K, V>* Find(const K& key);
		bool Erase(const K& key);

	private:
		vector<HashData<K, V>> _tables;
		size_t _n = 0;//存储的数据的个数
	};
}

构造函数HashTable()

先进行将哈希表初始化为,起初最多存储10个数据。

HashTable()
{
	_tables.resize(10);
}

插入数据Insert()

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

	// 扩容
	if (_n * 10 / _tables.size() >= 7)
	{
		//vector<HashData<K, V>> newTables(_tables.size() * 2);
		 遍历旧表, 将所有数据映射到新表
		 ...
		//_tables.swap(newTables);

		HashTable<K, V, Hash> newHT;
		newHT._tables.resize(_tables.size() * 2);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
			{
				newHT.Insert(_tables[i]._kv);
			}
		}

		_tables.swap(newHT._tables);
	}

	Hash hs;
	size_t hashi = hs(kv.first) % _tables.size();
	while (_tables[hashi]._state == EXIST)
	{
		++hashi;
		hashi %= _tables.size();
	}

	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	++_n;

	return true;
}

查找数据Find()

HashData<K, V>* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size();
	while (_tables[hashi]._state != EMPTY)
	{
		if (_tables[hashi]._state == EXIST
			&& _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];
		}

		++hashi;
		hashi %= _tables.size();
	}

	return nullptr;
}

删除数据Erase()

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

完整代码

//openress开散列定址法
namespace open_address
{
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};

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

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V> newHT;
				newHT._tables.resize(_tables.size() * 2);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);
			}
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			_n++;
			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._kv.first == key
					&& _tables[hashi]._state == EXIST)
				{
					return &_tables[hashi];
				}
				else
				{
					hashi++;
					hashi %= _tables.size();
				}
			}
			return nullptr;
		}

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

	private:
		vector<HashData<K, V>> _tables;
		size_t _n;
	};

	void Hash_open_address_Test1()
	{
		HashTable<int, int> ht;
		int a[] = { 6,23,13,4,2,32,43,56,4,1 };
		for (auto e : a)
		{
			ht.Insert({ e,e });
		}
		ht.Insert({ 9,9 });
		for (auto e : a)
		{
			ht.Erase(e);
		}
	}
	void Hash_open_address_Test2()
	{
		HashTable<int, int> ht;
		int a[] = { 11,21,4,14,24,15,9 };
		for (auto e : a)
		{
			ht.Insert({ e,e });
		}
		ht.Insert({ 34,34 });


		cout << ht.Find(24) << endl;
		cout << ht.Find(34) << endl;
	}

	void Hash_open_address_Test3()
	{
		HashTable<string, string> ht;
		ht.Insert({ "crush","喜欢" });
		ht.Insert({ "sheep","目前喜欢" });
		ht.Insert({ "iredq","目前喜欢" });
		ht.Insert({ "hsepe","目前喜欢" });
	}
}

哈希表开散列哈希桶——hash_bucket

hash_bucket的HashTable的结构体以及待实现的函数

namespace hash_bucket
{
	template<class K,class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		//存储的是为hashNode的节点
		pair<K, V> _kv;
		//就是存储的pair类型的数据

		HashNode(const pair<K, V>& kv)
			//哈希节点的构造函数,将这个生成为一个结构体
			:_kv(kv)
			,_next(nullptr)
		{}
	};

	template<class K,class V,class Hash = HashFunc<K>>
	//哈希表的实现
	//此时的模版类型有三个,因为有一个是解决string的映射问题
	class HashTable
	{
		typedef HashNode<K, V> Node;
		//本来节点的类型应该为HashNode<K,V>,但是为了方便
		//typedef重命名使其更为简单
	public:
		HashTable();//构造函数
		~HashTable();
		bool Insert(const pair<K, V>& kv);
		Node* Find(const K& key);
		bool Erase(const K& key);

	private:
		vector<Node*> _tables;
		size_t _n;
	};
}

构造函数HashTable()

HashTable()//构造函数
	//将每个表初始化可存储数据的个数
{
	_tables.resize(10, nullptr);
}

析构函数~HashTable()

~HashTable()
	//析构函数
	//无论何种的类型的析构函数都不会对内置类型做任何处理
	//vector中存储的是自定义类型的对象,
	//且这些自定义类型需要进行特定的资源清理操作,
	//这些自定义类型需要提供自己的析构函数来确保资源的正确释放
{
	//依次把每个桶释放
	for (size_t i = 0; i < _tables.size(); i++)
		//遍历哈希表中的每一个哈希桶
	{

		Node* cur = _tables[i];
		while (cur)
			//如果当前哈希表的当前哈希桶存储的节点的指针不为nullptr
			//那么就进行遍历
		{
			//先存储下一个节点
			Node* next = cur->_next;
			//释放当前节点
			delete cur;
			//循环遍历下一个节点
			cur = next;
		}
		//将下面的节点释放结束之后,不要忘记将哈希桶中存储的指针地址更改为nullptr
		_tables[i] = nullptr;
	}
}

插入数据Insert()

//插入数据pair键值对
bool Insert(const pair<K, V>& kv)
	//键值对是不能修改的
{
	Hash hs;
	size_t hashi = hs(kv.first) % _tables.size();

	if (_n == _tables.size())
	{
		//这里就不需要和开散列那样复用insert,
		//因为可能会出现这种情况:
		//假如需要很多的节点,那么就需要new很多节点
		//此时会出现空间消耗浪费
		//所以还是使用for循环遍历即可

		vector<Node*> newtables(_tables.size() * 2);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				Hash hs;
				size_t hashi = hs(cur->_kv.first) % newtables.size();
				cur->_next = newtables[hashi];
				//这里一定要理解链表中节点的链接方式
				//节点的链接方式的本质是没有箭头的
				//我们只是根据节点中存储的第一个节点的地址
				//意想为有箭头
				newtables[hashi] = cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}
	//先将需要插入的值,成为一个节点
	Node* newnode = new Node(kv);
	//此时newnode就有属性:pair<K,V> 与 Node* 存储下一个节点地址的属性
	newnode->_next = _tables[hashi];
	//这里注意整个类名是HashTable,但是vector中任然叫_tables
	//而tables[i]内部也只是存储了Node*类型
	_tables[hashi] = newnode;
	++_n;

	return true;
}

查找数据Find()

Node* Find(const K& key)
	//根据key关键值找对应的键值对
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size();
	//先进行映射,查看是否对应在映射的哪个位置
	Node* cur = _tables[hashi];
	//找到对应映射的哈希桶,进行遍历哈希桶
	while (cur)
	{
		//一直遍历哈希桶,确定是否相等
		//如果相等就直接返回当前节点
		if (cur->_kv.first == key)
		{
			return cur;
		}
		//如果不相等的话就直接,判断下一个节点是否相等
		else
		{
			cur = cur->_next;
		}
	}
	//如果遍历一直到空,此时任然没有找到相同节点的值,那么说明并没有这个节点
	//就返回空
	return nullptr;
}

删除数据Erase()

//删除指定的节点
bool Erase(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size();
	//找到当前节点对应的哈希桶
	//要删除节点:单链表的某个节点,
	// 那么一定是要当前节点cur的前一个节点prev的下一个节点指向当前节点的next
	// 所以需要记录两个节点
	Node* cur = _tables[hashi];
	//从哈希桶从上往下进行遍历,此时注意前一个节点也不是哈希桶中,而是空
	Node* prev = nullptr;

	//一直找一直找节点:找到对应的值相同的节点
		//每一次遍历,都要进行记录对应的节点的前一个节点并且如果不相等就让当前节点走下一个节点
	//如果找到了对应的节点
		//如果当前节点是头结点(前节点prev为空):
		//	那么直接让哈希桶存储的节点的地址更新为cur的next的节点地址即可
		//如果当前节点不是头结点(prev不为空):
		//	那么此时就是直接prev的next指向cur的next即可

	//然后对应的cur节点就直接进行释放,并且减少存储的数据的个数

	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}

			_n--;
			delete cur;
			return true;
		}
		else 
		{
			prev = cur;
			cur = cur->_next;
		}
		
	}
	return false;
}

完整代码

//哈希桶
namespace hash_bucket
{
	template<class K,class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		//存储的是为hashNode的节点
		pair<K, V> _kv;
		//就是存储的pair类型的数据

		HashNode(const pair<K, V>& kv)
			//哈希节点的构造函数,将这个生成为一个结构体
			:_kv(kv)
			,_next(nullptr)
		{}
	};

	template<class K,class V,class Hash = HashFunc<K>>
	//哈希表的实现
	//此时的模版类型有三个,因为有一个是解决string的映射问题
	class HashTable
	{
		typedef HashNode<K, V> Node;
		//本来节点的类型应该为HashNode<K,V>,但是为了方便
		//typedef重命名使其更为简单
	public:

		HashTable()//构造函数
			//将每个表初始化可存储数据的个数
		{
			_tables.resize(10, nullptr);
		}

		~HashTable()
			//析构函数
			//无论何种的类型的析构函数都不会对内置类型做任何处理
			//vector中存储的是自定义类型的对象,
			//且这些自定义类型需要进行特定的资源清理操作,
			//这些自定义类型需要提供自己的析构函数来确保资源的正确释放
		{
			//依次把每个桶释放
			for (size_t i = 0; i < _tables.size(); i++)
				//遍历哈希表中的每一个哈希桶
			{

				Node* cur = _tables[i];
				while (cur)
					//如果当前哈希表的当前哈希桶存储的节点的指针不为nullptr
					//那么就进行遍历
				{
					//先存储下一个节点
					Node* next = cur->_next;
					//释放当前节点
					delete cur;
					//循环遍历下一个节点
					cur = next;
				}
				//将下面的节点释放结束之后,不要忘记将哈希桶中存储的指针地址更改为nullptr
				_tables[i] = nullptr;
			}
		}

		//插入数据pair键值对
		bool Insert(const pair<K, V>& kv)
			//键值对是不能修改的
		{
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();

			if (_n == _tables.size())
			{
				//这里就不需要和开散列那样复用insert,
				//因为可能会出现这种情况:
				//假如需要很多的节点,那么就需要new很多节点
				//此时会出现空间消耗浪费
				//所以还是使用for循环遍历即可

				vector<Node*> newtables(_tables.size() * 2);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						Hash hs;
						size_t hashi = hs(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						//这里一定要理解链表中节点的链接方式
						//节点的链接方式的本质是没有箭头的
						//我们只是根据节点中存储的第一个节点的地址
						//意想为有箭头
						newtables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}
			//先将需要插入的值,成为一个节点
			Node* newnode = new Node(kv);
			//此时newnode就有属性:pair<K,V> 与 Node* 存储下一个节点地址的属性
			newnode->_next = _tables[hashi];
			//这里注意整个类名是HashTable,但是vector中任然叫_tables
			//而tables[i]内部也只是存储了Node*类型
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

		Node* Find(const K& key)
			//根据key关键值找对应的键值对
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			//先进行映射,查看是否对应在映射的哪个位置
			Node* cur = _tables[hashi];
			//找到对应映射的哈希桶,进行遍历哈希桶
			while (cur)
			{
				//一直遍历哈希桶,确定是否相等
				//如果相等就直接返回当前节点
				if (cur->_kv.first == key)
				{
					return cur;
				}
				//如果不相等的话就直接,判断下一个节点是否相等
				else
				{
					cur = cur->_next;
				}
			}
			//如果遍历一直到空,此时任然没有找到相同节点的值,那么说明并没有这个节点
			//就返回空
			return nullptr;
		}

		//删除指定的节点
		bool Erase(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			//找到当前节点对应的哈希桶
			//要删除节点:单链表的某个节点,
			// 那么一定是要当前节点cur的前一个节点prev的下一个节点指向当前节点的next
			// 所以需要记录两个节点
			Node* cur = _tables[hashi];
			//从哈希桶从上往下进行遍历,此时注意前一个节点也不是哈希桶中,而是空
			Node* prev = nullptr;

			//一直找一直找节点:找到对应的值相同的节点
				//每一次遍历,都要进行记录对应的节点的前一个节点并且如果不相等就让当前节点走下一个节点
			//如果找到了对应的节点
				//如果当前节点是头结点(前节点prev为空):
				//	那么直接让哈希桶存储的节点的地址更新为cur的next的节点地址即可
				//如果当前节点不是头结点(prev不为空):
				//	那么此时就是直接prev的next指向cur的next即可

			//然后对应的cur节点就直接进行释放,并且减少存储的数据的个数

			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					_n--;
					delete cur;
					return true;
				}
				else 
				{
					prev = cur;
					cur = cur->_next;
				}
				
			}
			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n;
	};

	void Hash_bucket_test1()
	{
		HashTable<int, int> ht;
		int a[] = { 11,21,4,14,15,9,19,29,39 };
		for (auto e : a)
		{
			ht.Insert({ e,e });
		}
		ht.Insert({ 6,6 });

		for (auto e : a)
		{
			ht.Erase(e);
		}
	}

	void Hash_open_address_Test3()
	{
		HashTable<string, string> ht;
		ht.Insert({ "crush","喜欢" });
		ht.Insert({ "sheep","目前喜欢" });
		ht.Insert({ "iredq","目前喜欢" });
		ht.Insert({ "hsepe","目前喜欢" });
	}
}

解决存储的数据并非整形转整形进行取模计算

//某些类型是不能直接转为int类型的,
//就必须通过其强制类型转换为整形
template<class K>
//因为进行取模的是key关键值,所以只需要一个模版参数
//将这个函数方法封装为一个结构体
//使用的时候直接是构造一个结构体,然后进行使用操作符

struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//特化string
template<>
//如果说此时的数据类型是string,那么此时也不能使用强制转换
//特化:特殊情况特殊处理
struct HashFunc<string>
{
	//模版参数直接为空,结构体名称后直接跟模版参数类型
	size_t operator()(const string& key)
		//形参中已经确定是string类型了
	{
		//此时初始化hash为0
		size_t hash = 0;
		for (auto e : key)
			//依次遍历字符串中的字符
		{
			//为了尽量减少哈希冲突*31+字符串的值
			hash *= 31;
			hash += e;
		}
		return hash;
	}
};

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

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

相关文章

【机器学习算法基础】(基础机器学习课程)-10-逻辑回归-笔记

一、模型的保存与加载 逻辑回归是一种常见的机器学习算法&#xff0c;广泛用于分类问题。为了在不同的时间或环境下使用训练好的模型&#xff0c;我们通常需要将其保存和加载。 保存模型 训练模型&#xff1a;首先&#xff0c;你需要用你的数据训练一个逻辑回归模型。例如&…

【软考】甘特图

目录 1. 说明2. 图示3. 特点4. 例题4.1 例题1 1. 说明 1.Gantt图是一种简单的水平条形图,它以日历为基准描述项目任务。2.水平轴表示日历时间线(如时、天、周、月和年等)&#xff0c;每个条形表示一个任务&#xff0c;任务名称垂直地列在左边的列中&#xff0c;图中水平条的起…

猫头虎分享:从零开始掌握ChatGPT的实用技巧与多样应用

猫头虎分享&#xff1a;从零开始掌握ChatGPT的实用技巧与多样应用 ChatGPT使用方法与应用场景分享 大家好&#xff0c;我是猫头虎 &#x1f42f;&#xff0c;欢迎大家来到这次的分享课程。在这里&#xff0c;我们将深入了解ChatGPT的使用方法和应用场景。本文旨在帮助大家从零…

配置nacos显示nacos registry register finished但是nacos页面看不到服务

在idea配置按以下配置&#xff1a; 父工程&#xff1a; <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2021.0.1.0</version><type>pom</type&…

内网权限维持——映像劫持CLR劫持

文章目录 一、映像劫持1.1 IFEO简介1.2 利用Shfit后门技术进行劫持1.3 GlobalFlag 二、CLR劫持2.1 CLR简介2.2 利用CLR探查器进行权限维持 攻击机kali IP&#xff1a;192.168.111.0 跳板机win7 IP&#xff1a;192.168.111.128&#xff0c;192.168.52.143 靶机win server 2008 I…

【开端】通过springboot框架创建对象

日常创建对象的方式 UserService userService new UserService() 这中方式创建对象&#xff0c;是程序运行中&#xff0c;才会创建的对象。在web中&#xff0c;我们需要再web服务器启动完成就创建一系列的对象。这是就可以把创建对象的任务交给spring的IOC框架。 例如创建U…

SOMEIP_ETS_003:数组长度过短导致有效载荷被剥离

测试目的&#xff1a; 确保DUT在接收到的SOME/IP消息中数组长度小于实际数组长度时&#xff0c;能够正确地截断负载数据至声明的数组长度。 描述 本测试用例旨在验证DUT在处理一个声明数组长度小于其实际长度的SOME/IP消息时&#xff0c;是否能够将响应消息的负载数据截断至…

国标GB/T28181视频转S3云存储,支持阿里云OSS、腾讯云COS、天翼云存储,视频转云存大大降低运营商项目运营成本

最近在做一个运营商主导的项目&#xff0c;在沟通项目需求的时候&#xff0c;发现从运营商的角度&#xff0c;带宽和存储的成本在内部计费中是能够比市场上的价格低的多的多&#xff0c;以一个100路摄像头的本地存储为例&#xff0c;如果采用NVR本地存储&#xff0c;或者CVR本地…

LabVIEW水下根石监测系统

开发了一种基于LabVIEW平台开发的水下根石监测系统。该系统利用高精度姿态传感器与位移传感器&#xff0c;实现了水下根石状态的实时自动监测&#xff0c;提高了水利工程安全管理的现代化和精细化水平&#xff0c;具有高精度、高稳定性和良好的操作性。 项目背景&#xff1a; …

计算机体系结构和计算机组成原理的区别

如何理解计算机体系结构和计算机的组成&#xff1f;哪个对计算机的性能更重要&#xff1f;说明理由 目录 计算机体系结构 计算机组成 二者区别 哪个对性能更重要 计算机体系结构 计算机体系结构是指根据属性和功能不同而划分的计算机理论组成部分及计算机基本工作原理、理论…

Linux系统驱动(二)字符设备驱动

文章目录 一、概念&#xff08;一&#xff09;相关概念&#xff08;二&#xff09;字符设备框架结构&#xff08;三&#xff09;用户空间和内核空间数据传输1. 函数的参数对应关系 &#xff08;四&#xff09;字符设备相关的API1. 字符设备驱动&#xff08;1&#xff09;注册字…

stl容器 vector的使用与模拟实现

1.vector构造 1.1默认构造函数 vector<int>是vector类模版类型&#xff0c;尖括号里的类型是指生成什么类型的vector的类&#xff0c;实质上vector可以看做一个数组&#xff0c;vector<int>实质上就是生成了一个存int类型的数组&#xff0c;而tamp是这个数组的名字…

SpringSecurity-2(认证和授权+SpringSecurity入门案例+自定义认证+数据库认证)

SpringSecurity使用自定义认证页面 4 SpringSecurity使用自定义认证页面4.1 在SpringSecurity主配置文件中指定认证页面配置信息4.1.1 配置springSecurity.xml配置文件4.1.2 定义login.jsp 4.2 SpringSecurity的csrf防护机制4.2.1 SpringSecurity中CsrfFilter过滤器说明4.2.2 禁…

苹果iPhone 16 Pro系列有望支持Wi-Fi 7,再也不说苹果信号不好了

苹果公司始终以其创新技术引领智能手机市场的发展。 随着新一代iPhone 16系列的即将发布&#xff0c;特别是iPhone 16 Pro系列&#xff0c;预计将带来一系列令人瞩目的升级和新功能。 其中最引人注目的是Wi-Fi 7技术的应用&#xff0c;这将为用户带来前所未有的无线网络速度。…

计算机毕业设计选题推荐-校园服务系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

飞书API 2-7:如何将 MySQL 数据库的查询结果写入多维表(下)

一、引入 上一篇&#xff0c;解决了数据持续插入更新的问题。在一些场景下&#xff0c;如果数据量较大&#xff0c;需要跑多个任务调用接口插入&#xff0c;但是逐个跑任务又太久&#xff0c;又该怎么提高执行速度呢&#xff1f;没错&#xff01;就是多线程。 本文就来探讨下…

【Nuxt】服务端渲染 SSR

SSR 概述 服务器端渲染全称是&#xff1a;Server Side Render&#xff0c;在服务器端渲染页面&#xff0c;并将渲染好HTML返回给浏览器呈现。 SSR应用的页面是在服务端渲染的&#xff0c;用户每请求一个SSR页面都会先在服务端进行渲染&#xff0c;然后将渲染好的页面&#xf…

STM32 | ADC+RS485(第十天)

点击上方"蓝字"关注我们 01、ADC概述 ADC, Analog-to-Digital Converter的缩写,指模/数转换器或者模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号.例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的…

【公考新手教程】公考新手小白备考规划

公考 公考相关考试国考省考 行测常识判断言语理解与表达数量关系判断推理&#xff08;重中之重&#xff09;图推定义判断类比逻辑判断 资料分析&#xff08;重中之重&#xff09; 申论&#xff08;很重要&#xff0c;提升困难&#xff09;公基推荐考公软件粉笔华图在线bilibili…

零基础开始学习鸿蒙开发-文章推荐栏获取接口数据并展示

目录 1.新建文章列表布局页面&#xff0c;通过静态数据&#xff0c;编写好布局页面。 1.1 通过行ArticleCard布局构建单个文章展示的item项 1.2 使用了ObjectLink装饰器&#xff08;尽管这不是ArkUI标准API的一部分&#xff0c;特定框架或自定义的扩展&#xff09;&#xff0c…