C++数据结构 -- 哈希表

news2024/11/19 1:23:38

目录

  • 一、哈希概念
  • 二、 哈希冲突
  • 三、 哈希函数
  • 四、 减少哈希冲突常用的方法
    • 4.1 闭散列
      • 4.1.1 闭散列的开放定址法的增容
      • 4.1.2 闭散列的开放定址法的哈希结构的实现
    • 4.3 开散列
      • 4.3.1 开散列概念
      • 4.3.2 插入元素
      • 4.3.2 删除元素
      • 4.3.3 开散列的哈希桶的增容
      • 4.3.4 开散列的哈希桶(拉链法)代码实现
      • 4.3.5 关于开散列的思考
      • 4.3.6 开散列与闭散列比较

一、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),而搜索的效率取决于搜索过程中元素的比较次数。
所以理想的搜索方法是:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

插入元素 :根据待插入元素的关键码,按照此函数计算出该元素的存储位置并按此位置进行存放

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

以上方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如插入:11,3,4,25
在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。
那上述的哈希表中再插入一个1呢?通过计算hash(1)%10=1,也就是说1应该插入到下标为1的位置中,但是hash[1]已经插入了11了。

二、 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即:不同关键字通过相同哈希算法计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。那么发生哈希冲突该如何处理呢?

三、 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
2、哈希函数计算出来的地址能均匀分布在整个空间中。
3、哈希函数应该比较简单。

常见哈希函数:

  1. 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:
    Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况

  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  3. 平方取中法–(了解)
    假设关键字为2345,对它平方就是5499025,抽取中间的3位990作为哈希地址;
    再比如关键字为5432,对它平方就是29506624,抽取中间的3位506作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

  4. 折叠法–(了解一下即可)
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

  5. 随机数法–(了解一下即可)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
    通常应用于关键字长度不等时采用此法

  6. 数学分析法–(了解一下)
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:
    假设要存储班级学生的登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现冲突,还可以对抽取出来的数字进行反转(如5678改成8765)、右环位移(如5678改成8567)、左环移位、前两数与后两数叠加(如5678改成56+78=134)等方法。
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

值得注意的是:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

我们在以下的内容中是使用除留余数法实现哈希表的。

四、 减少哈希冲突常用的方法

闭散列和开散列。

4.1 闭散列

闭散列,也称开放定址法。当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
1、线性探测
插入元素:
如果计算出来的位置已经有元素,那么就通过线性探测往后找到一个空位置插入即可。
在这里插入图片描述
在这里插入图片描述

删除元素:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

所以对于闭散列的哈希表的存放的数据中应该包含一个标志状态的变量,新插入的节点的状态标志位是EXIST,被删除的节点的标志位设置为DELETE,没插入的节点的标志位默认设置为EMPTY。

线性探测优点:实现非常简单。
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。那么这种情况该如何缓解呢?

2、二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。简单来说就是在往后找空位置的时候不是直接找下一个位置,而是找下一个位置平方对应的位置。这样就能更好地缓解冲突的数据全部堆积在一块的问题了。

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

因此:闭散列最大的缺陷就是空间利用率比较低,基本上浪费了50%的空间,同时这也是哈希的缺陷。

4.1.1 闭散列的开放定址法的增容

哈希表会在什么情况下扩容?如何扩容?
在这里插入图片描述

			//负载因子(有效数据在哈希表中的占比)超过70%就进行扩容
			if (_n * 10 / _table.size() >= 7)
			{
				//扩容
				size_t newSize = _table.size() * 2;
				//这里直接定义一个新的哈希表对象,好处是能够直接复用这个Insert函数,
				//最后进行指针的交换,为什么这里适合复用Insert函数,因为闭散列的开
				//放定址法的核心就是线性探测,如果这里不复用Insert自己写的话,跟下面
				//线性探测的逻辑是一模一样的,所以没必要重新写
				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSize);
				for (size_t i = 0; i < _table.size(); i++)
				{
					//把旧表中的存在的数据插入到新的哈希表
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}
				//两个vector交换,本质是交换vector的_start,_finish,_endofstorage指针,刚好还把
				//旧哈希表中_start,_finish,_endofstorage指针交换给newTable,newTable是局部对象,
				//出了作用域自动销毁,刚好满足我们的需求
				_table.swap(newHT._table);
			}

4.1.2 闭散列的开放定址法的哈希结构的实现

#include <iostream>
using namespace std;
#include <vector>

//仿函数
template <class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		//这里强转可以解决负数的映射问题
		return (size_t)key;
	}
};

//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t ret = 0;
		for (const auto& ch : s)
		{
			//这个131是别人经过研究计算出来的一个值
			//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)
			ret *= 131;
			ret += ch;
		}
		return ret;
	}
};


//闭散列的开放定址法
namespace open_address
{
	enum State
	{
		//有效值
		EXIST,
		//无效值
		DELETE,
		//空
		EMPTY
	};

	//闭散列存放的数据是一个pair和这个位置的pair的状态,因为删除的时候不能把这个值去掉,所以
	//任何操作都是改变这个数据的状态而已
	template <class K,class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;//节点的默认状态应该是EMPTY的,当插入或者删除值的时候再修改
	};

	template <class K,class V,class HashFunc= DefaultHashFunc<K>>
	class HashTable
	{
	public:
		//构造函数
		HashTable()
		{
			//提前开辟10个空间
			_table.resize(10);
		}

		//防止外面通过指针修改key值,所以HashData<const K, V>*加上const
		HashData<const K, V>* Find(const K& key)
		{
			HashFunc hf;

			//先到key映射的下标中找,如果找到了就直接返回,如果没找到就往后找
			//找到最后没找到就绕回来前面找,直到找到EMPTY还没有找到就说明这个
			//节点不存在了
			//这里的key不一定是整形,而只有整形的数才能取模,所以我们要通过仿函数
			//把这个数据的key值变成整形然后取出来,这样才能支持取模
			size_t hashi = hf(key) % _table.size();
			//遇到删除状态为DELETE的也要继续找,因为DELETE的节点的后面(或者前面)还有值
			while (_table[hashi]._state != EMPTY)
			{
				//找到了就返回地址
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
				{
					//这里需要强转一下,因为&_table[hashi]的类型是HashData<K,V>*
					return (HashData<const K, V>*)&_table[hashi];
				}
				hashi++;
				//取模一下可以从最后一个位置绕回来到第一个位置
				hashi %= _table.size();
			}
			//走到这里说明找不到这个key了,返回空
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<const K, V>* ret = Find(key);
			if (ret)
			{
				//删除操作就是把该节点的状态改成DELETE即可,无需修改它的值,因为你也不知道该改成什么值
				ret->_state = DELETE;
				_n--;
				return true;
			}
			else
			{
				return false;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			HashFunc hf;
			//如果插入的节点存在,那么就不用插入了
			if (Find(kv.first) != nullptr)
			{
				return false;
			}
			//负载因子(有效数据在哈希表中的占比)超过70%就进行扩容
			if (_n * 10 / _table.size() >= 7)
			{
				//扩容
				size_t newSize = _table.size() * 2;
				//这里直接定义一个新的哈希表对象,好处是能够直接复用这个Insert函数,
				//最后进行指针的交换,为什么这里适合复用Insert函数,因为闭散列的开
				//放定址法的核心就是线性探测,如果这里不复用Insert自己写的话,跟下面
				//线性探测的逻辑是一模一样的,所以没必要重新写
				HashTable<K, V, HashFunc> newHT;
				newHT._table.resize(newSize);
				for (size_t i = 0; i < _table.size(); i++)
				{
					//把旧表中的存在的数据插入到新的哈希表
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}
				//两个vector交换,本质是交换vector的_start,_finish,_endofstorage指针,刚好还把
				//旧哈希表中_start,_finish,_endofstorage指针交换给newTable,newTable是局部对象,
				//出了作用域自动销毁,刚好满足我们的需求
				_table.swap(newHT._table);
			}

			//计算映射的位置(下标)
			//这里模的必须是_table.size(),因为_table是vector结构,有效的访问的区间是[0,_table.size()),
			//如果这里模的是_table.capacity(),那么得到的值可能大于_table.size(),被看作为越界访问
			size_t hashi = hf(kv.first) % _table.size();
			//线性探测
			//找到下一个被删除的或者为空的位置(可能是hashi本身,如果不是就往后找)
			while (_table[hashi]._state == EXIST)
			{
				hashi++;
				//取模一下可以从最后一个位置绕回来到第一个位置继续找,我们的结构的设计保证了这里一定能够
				//找到一个空(或者被删除了的)位置,因为有效值的占比超过70%就会扩容了,所以这个数组不可能被填满的
				hashi %= _table.size();
			}
			//走到这里说明一定找到了一个空(或者被删除了的)位置,因为数组的元素本身已经初始化成了HashData<K,V>的默认值,
			//所以我们修改这个节点的pair和状态即可
			_table[hashi]._kv = kv;
			//把这个位置的节点的状态值标志位EXIST,表示这是一个有效值
			_table[hashi]._state = EXIST;
			_n++;
			return true;
			
		}

		//打印函数
		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXIST)
				{
					cout << _table[i]._kv.first << ":" << _table[i]._kv.second << endl;
				}
			}
			cout << endl;
		}

	private:
		vector<HashData<K, V>> _table;
		//记录插入的有效数据的个数
		size_t _n = 0;
	};
}

4.3 开散列

4.3.1 开散列概念

开散列法又叫链地址法(拉链法或者哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

4.3.2 插入元素

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3.2 删除元素

就是先计算要删除的元素在数组中的哪一个位置,再在这个位置对应的单链表中查找要删除的元素并删除即可,本质还是单链表的删除。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3.3 开散列的哈希桶的增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

			//扩容,插入的有效节点数等于数组的大小就扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				//这里还适合定义一个新的对象,然后复用这个Insert函数插入旧表中的所有值吗?
				//这里就不适合了,为什么呢?
				// 1、因为这里数组中存放的是节点的指针,如果复用这个
				//Insert函数,在Insert的逻辑中就会重新开辟出所有的节点插入到这个新对象,
				// 然后还要自己把旧表中的所有的节点释放掉,反而变得麻烦了。
				// 2、并且这里的插入并不需要线性探测找位置,直接头插到数组对应的位置就行了,所以
				//这里可以选择直接开辟一个vector数组,然后把旧表中的所有节点都直接转移到这个vector
				//这样新的vector和旧的vector交换就可以了,因为这里是转移节点,所以旧的vector中已
				//经没有节点了,此时无需再自己手动释放掉旧的节点
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						//计算旧节点在新的vector中的位置
						size_t hashi = hf(cur->_kv.first) % newTable.size();
						//把节点从旧表中转移头插到新表
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					//最后记得把旧表的该位置置空,免得后面非法访问
					_table[i] = nullptr;
				}
				//最后交换两个vector表即可
				_table.swap(newTable);
			}

4.3.4 开散列的哈希桶(拉链法)代码实现

//仿函数
template <class K>
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		//这里强转可以解决负数的映射问题
		return (size_t)key;
	}
};

//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t ret = 0;
		for (const auto& ch : s)
		{
			//这个131是别人经过研究计算出来的一个值
			//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)
			ret *= 131;
			ret += ch;
		}
		return ret;
	}
};


//拉链法,哈希桶
namespace hash_bucket
{
	//拉链法中数组存放的是一个pair和_next指针的节点
	template <class K,class V>
	struct HashNode
	{
		pair<const K, V> _kv;
		HashNode<K, V>* _next;

		HashNode(const pair<const K,V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};

	template <class K,class V,class HashFunc= DefaultHashFunc<K>>
	class HashTable
	{
		//节点
		typedef HashNode<K, V> Node;
	public:
		//构造函数
		HashTable()
		{
			//初始化数组,因为数组就是插入节点的指针的,所以可以先把整个数组初始化成空指针即可
			_table.resize(10, nullptr);
		}

		//析构函数
		~HashTable()
		{
			//需要把整个数组中所有位置的链表的所有节点全部释放
			//等于是遍历一遍这个指针数组
			for (size_t i = 0; i < _table.size(); i++)
			{
				//把数组中所有不为空的位置的链表的节点遍历释放
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				//释放完某个位置后记得置空一下,免得后面非法访问
				_table[i] = nullptr;
			}
		}

		Node* Find(const K& key)
		{
			//利用仿函数把key变成可以取模的值,因为key可能是string等本身不能取模的类型
			HashFunc hf;
			//像函数一样使用这个仿函数,利用的一定的规则把key转换成某个整形,以便取模求出
			//key值应该存储的位置的下标
			size_t hashi = hf(key) % _table.size();
			//遍历这个下标对应位置的链表,如果key存在,则一定在这个链表中
			Node* cur = _table[hashi];
			//遍历该key映射的数组位置的链表查找key值的节点
			while (cur)
			{
				//找到了直接返回这个节点指针
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			//找不到返回空
			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			//如果该kv已经存在,就不再插入了,直接返回
			if (Find(kv.first)!=nullptr)
			{
				return false;
			}

			HashFunc hf;

			//扩容,插入的有效节点数等于数组的大小就扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				//这里还适合定义一个新的对象,然后复用这个Insert函数插入旧表中的所有值吗?
				//这里就不适合了,为什么呢?
				// 1、因为这里数组中存放的是节点的指针,如果复用这个
				//Insert函数,在Insert的逻辑中就会重新开辟出所有的节点插入到这个新对象,
				// 然后还要自己把旧表中的所有的节点释放掉,反而变得麻烦了。
				// 2、并且这里的插入并不需要线性探测找位置,直接头插到数组对应的位置就行了,所以
				//这里可以选择直接开辟一个vector数组,然后把旧表中的所有节点都直接转移到这个vector
				//这样新的vector和旧的vector交换就可以了,因为这里是转移节点,所以旧的vector中已
				//经没有节点了,此时无需再自己手动释放掉旧的节点
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						//计算旧节点在新的vector中的位置
						size_t hashi = hf(cur->_kv.first) % newTable.size();
						//把节点从旧表中转移头插到新表
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					//最后记得把旧表的该位置置空,免得后面非法访问
					_table[i] = nullptr;
				}
				//最后交换两个vector表即可
				_table.swap(newTable);
			}

			//计算新插入节点在表中的位置
			size_t hashi = hf(kv.first) % _table.size();
			Node* newNode = new Node(kv);
			//头插到对应位置的单链表中
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			_n++;
			return true;
		}

		bool Erase(const K& key)
		{
			HashFunc hf;
			//计算删除元素在表中的位置
			size_t hashi = hf(key) % _table.size();

			//遍历表中对应下标位置的单链表找删除元素
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				//找到删除元素
				if (cur->_kv.first == key)
				{
					//头删
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					//中间删除
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				printf("[%d]->", i);
				while (cur)
				{
					cout << "(" << cur->_kv.first << ":" << cur->_kv.second << ")" << "->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}

	private:
		vector<Node*> _table;//哈希表数组存放的是节点的指针,这个节点是一个链表的头节点
		size_t _n = 0;//记录插入的有效节点的个数
	};
}

4.3.5 关于开散列的思考

1、只能存储key为整形的元素,其他类型怎么解决?
因为我们计算元素在数组中存储的位置是通过除留余数法计算的,即用key模数组的大小得到的余数就是该元素在数组中的存储位置,也就意味着我们的key必须要是整形,因为只有整形才能取模,那么如果我们存储的key是一个字符串呢,字符串是不能取模的,这个时候我们该怎么处理呢?
如果我们要继续采用除留余数法计算key值对应的位置,我们就要想办法把这个字符串按照一定的规则转化成整形,再进行取模,这样就能得到这个字符串在数组中对应的存储位置了。
那么我们该如何把这个string转化成一个整形呢?
这个时候仿函数就要登场了,仿函数的作用在这种场景的就非常的好用,无论插入元素的key是什么类型的值我们都可以通过仿函数把它转化成整形,进而通过除留余数法计算出它在数组中的存储位置了。

//仿函数
template <class K>
struct DefaultHashFunc
{
	//针对于存储元素的key值是int类型的仿函数,返回值就是这个key本身
	size_t operator()(const K& key)
	{
		//这里强转可以解决负数的映射问题
		return (size_t)key;
	}
};

//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{
	//如果存储元素的key值是string,那么就把这个字符串的所有字符结合起来,算出一个整形
	size_t operator()(const string& s)
	{
		size_t ret = 0;
		for (const auto& ch : s)
		{
			//这个131是别人经过研究计算出来的一个值
			//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)
			ret *= 131;
			ret += ch;
		}
		return ret;
	}
};

2、除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

以下是别人通过测试计算出来的哈希冲突较小的哈希函数:

size_t GetNextPrime(size_t prime)
{
	const int PRIMECOUNT = 28;
	static const size_t primeList[PRIMECOUNT] =
	{
		53ul, 97ul, 193ul, 389ul, 769ul,
		1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
		49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
		1572869ul, 3145739ul, 6291469ul, 12582917ul,
		25165843ul,
		50331653ul, 100663319ul, 201326611ul, 402653189ul,
		805306457ul,
		1610612741ul, 3221225473ul, 4294967291ul
	};
	size_t i = 0;
	for (; i < PRIMECOUNT; ++i)
	{
		if (primeList[i] > prime)
		return primeList[i];
	}
	
	return primeList[i];
}

4.3.6 开散列与闭散列比较

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

以上就是今天想要跟大家分享的内容啦,你学会了吗?如果感觉到有所帮助,你就点点赞点点关注呗,后期还会持续更新C++相关的知识哦,我们下期见啦!!!!!

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

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

相关文章

VB将十进制整数转换成16进制以内的任意进制数

VB将十进制整数转换成16进制以内的任意进制数 数值转换&#xff0c;能够将十进制整数转换成16进制以内的任意进制数 Private Function DecToN(ByVal x%, ByVal n%) As StringDim p() As String, y$, r%p Split("0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F", ",")I…

基于Java学生档案管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

重磅发布!汉威科技燃气安全一站式解决方案全面覆盖燃气安全最新需求

近年来&#xff0c;我国陆续出台相关国家政策、标准等文件&#xff0c;对燃气安全建设提出一系列新要求&#xff0c;尤其是近期国务院安委会印发的《全国城镇燃气安全专项整治工作方案》&#xff0c;将燃气安全建设提升至新的高度。要实现全链条整治并做到标本兼治&#xff0c;…

【Vue】浏览器自定义格式化工具

当我们不启用浏览器自定义格式化工具&#xff0c;输出 Ref 或者 Reactive 类的值出输出完整的返回值对象&#xff0c;而且 value 值需要再次点击才可以得到。 为了解决这个问题&#xff0c;我们可以开启浏览器自定义格式化工具。 点击 setting 齿轮 依次找到 preference ->…

【探索C++】C++对C语言的扩展

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff0…

30.链表练习题(1)(王道2023数据结构2.3.7节1-8题)

【前面使用的所有链表的定义在第29节】 试题1&#xff1a; 设计一个递归算法&#xff0c;删除不带头结点的单链表L中所有值为x的结点。 首先来看非递归算法&#xff0c;暴力遍历&#xff1a; int Del(LinkList &L,ElemType x){ //此函数实现删除链表中为x的元素LNode *…

oracle分组排序取第一条数据

row_number()over(partition by 分组字段 order by 排序字段) 例&#xff1a;select * from ( select id,dealer_name,row_number()over(partition by dealer_name order by create_time ) r from mr) where r1

欧盟对中反补贴引发恐慌 | 百能云芯

中德汽车产业链紧密交织&#xff0c;欧盟对中国电动汽车反补贴的调查可能对欧洲本土产业造成负面影响。欧盟计划到2035年停售非零碳排放汽车&#xff0c;这为中国电动汽车在欧洲市场寻求机会提供了契机&#xff0c;但中国电动车的快速崛起引发了国际社会的警惕。 欧盟委员会主席…

在比特币上使用可检索性证明支付存储费用

我们为用户开发了一种为云存储付费的新方法。 与亚马逊的 S3 等传统云存储相比&#xff0c;用户不必信任服务器。 我们使用比特币智能合约来确保支付取决于服务器的可检索性证明 (PoR)&#xff0c;该证明只能在数据仍然可用且需要时可以检索的情况下生成。 可检索性证明 (PoR)…

WireShark抓包分析TCP三次握手过程,TCP报文解析

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 使用WireShark工具抓取TCP协议三次握手的数据包&am…

06-数据库检索:如何使用B-树对海量磁盘数据建立索引?

06-数据库检索&#xff1a;如何使用B-树对海量磁盘数据建立索引&#xff1f; 你好&#xff0c;我是陈东。 在基础篇中&#xff0c;我们学习了许多和检索相关的数据结构和技术。但是在大规模的数据环境下&#xff0c;这些技术的应用往往会遇到一些问题&#xff0c;比如说&…

CGAL安装到验证到深入

1、安装CGAL Win10下VS配置CGAL-5.3.1&#xff08;下载、安装、VS属性表配置&#xff09; 测试代码_cgal下载_孙 悟 空的博客-CSDN博客 2、CGAL验证练习 #include <iostream> #include <CGAL/Simple_cartesian.h> typedef CGAL::Simple_cartesian<double> …

800多个看图猜电视剧的含图的ACCESS\EXCEL数据库

虽然说看图猜什么是去年流行的一种手机游戏&#xff0c;但是这种游戏放置在任何年代都算是一种趣味之一&#xff0c;而今天看到一个800多的电视载图猜电视剧的内容&#xff0c;因此就弄下来&#xff0c;毕竟这种数据是永不过期的。 其实&#xff0c;实现迷惑字很简单&#xff0…

柔和舒适的瑜伽垫,设计时尚两面可用

日常健身的时候&#xff0c;瑜伽垫是个很实用的工具&#xff0c;可以大大提升健身时的舒适性&#xff0c;不过在选择瑜伽垫的时候也要注意质量&#xff0c;特别是像厚度、弹力、异味之类的细节&#xff0c;对平时使用的影响都很大。 目前我用的是一款莫比 联名扭蛋兔瑜伽垫&…

2023年前端面试真题之CSS篇

人的一生&#xff0c;总是难免有浮沉。不会永远如旭日东升&#xff0c;也不会永远痛苦潦倒。反复地一浮一沉&#xff0c;对于一个人来说&#xff0c;正是磨练。因此&#xff0c;浮在上面的&#xff0c;不必骄傲&#xff1b;沉在底下的&#xff0c;更用不着悲观。必须以率直、谦…

Knife4jInsight 1.0.0 MVP 版本发布

Knife4jInsight 1.0.0 MVP 版本发布 前言系统用户平台特性主要功能后期计划的功能 平台管理OpenAPI数据源接口文档自动i18n,支持中英双语微服务OpenAPI规范数据源自动注册上报整合开源swagger-ui组件&#xff0c;平台中可进行OpenAPI规范接口设计打通开源注册中心(Nacos\Eureka…

暨南大学旅游管理《乡村振兴战略下传统村落文化旅游设计》许少辉校友——2023学生开学季辉少许

暨南大学旅游管理《乡村振兴战略下传统村落文化旅游设计》许少辉校友——2023学生开学季辉少许

SpringCloud Alibaba - Sentinel

接上文SpringCloud Alibaba - Nacos 1.Sentinel 流量防卫兵 1.1 安装与部署 和Nacos一样&#xff0c;它是独立安装和部署的&#xff0c;下载地址https://github.com/alibaba/Sentinel/releases 下载后的jar放到目录 然后配置 启动并访问,用户名密码都是 sentinel 此时就…

ISP技术概述

原本或许是为了对冲手机系统和APP设计无力感而诞生的拍照功能,现今却成为了众手机厂家除背部设计外为数不多可“卷”的地方,自拍、全景、夜景、小视频等旺盛的需求让这一技术的江湖地位迅速变化。对圈内人士而言,这一波变化带来的后摄、双摄、多摄、暗光、防抖、广角、长焦、…

软件定制开发具有以下特点|APP搭建|小程序

软件定制开发具有以下特点|APP定制|小程序 一、快速响应用户需求 软件定制开发的优势在于&#xff0c;它可以快速响应用户的需求&#xff0c;因为它是在现有软件的基础上进行功能定制、界面定制、服务定制等改造&#xff0c;而不是从零开始进行重新设计与开发&#xff0c;所以…