C++进阶--哈希表的的闭散列和开散列(哈希桶)实现

哈希表的的闭散列和开散列(哈希桶)实现

  • 一、哈希概念
  • 二、哈希冲突
  • 三、哈希函数
    • 3.1 直接定址法--(常用)
    • 3.2 除留余数法--(常用)
    • 3.3 平方取中法--(了解)
    • 3.4 折叠法--(了解)
    • 3.5 随机数法--(了解)
    • 3.6 数学分析法--(了解)
  • 四、哈希冲突解决
    • 4.1 闭散列——开放定址法
      • 4.1.1 线性探测
      • 4.1.2 二次探测
    • 4.2 开散列——链地址法(拉链法、哈希桶)
  • 五、哈希表的闭散列实现
    • 5.1 哈希表的结构
    • 5.2 哈希表的插入
    • 5.3 哈希表的查找
    • 5.4 哈希表的删除
  • 六、哈希表的开散列实现(哈希桶)
    • 6.1 哈希表的结构
    • 6.2 哈希表的插入
    • 6.3 哈希表的查找
    • 6.4 哈希表的删除
  • 七、哈希表的大小为什么建议是素数
  • 八、完整代码
    • 8.1 HashTable.h
    • 8.2 test.cpp

一、哈希概念

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

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

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

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key)=key%capacity;capacity为存储元素底层空间总的大小。
在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

二、哈希冲突

  对于两个数据元素的关键字 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),即不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

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

三、哈希函数

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

哈希函数设计原则

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

3.1 直接定址法–(常用)

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

3.2 除留余数法–(常用)

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

3.3 平方取中法–(了解)

  假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

3.4 折叠法–(了解)

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

3.5 随机数法–(了解)

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

3.6 数学分析法–(了解)

  设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

在这里插入图片描述
  假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
  数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

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

四、哈希冲突解决

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

4.1 闭散列——开放定址法

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

4.1.1 线性探测

当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。

Hi=(H0+i)%m (i=1,2,3,…)

H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置。
m:表的大小。
例如:我们用除留余数法将序列插入列表长为10的哈希表中,当发生哈希冲突时我们采用闭散列的线性探测找到下一个空位置进行插入。

在这里插入图片描述
  随着哈希表中数据的增多,产生哈希冲突的可能性也随着增加,最后在1002进行插入的时候更是连续出现了四次哈希冲突。
  我们将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。介于此,哈希表当中引入了负载因子(载荷因子):

负载因子=表中有效数据个数/空间的大小

  • 负载因子越大,产生冲突的概率越高,增删改查的效率越低。
  • 负载因此越小,产生冲突的概率越低,增删改查的效率越高。

例如,我们将哈希表的大小改为20,可以看到在插入相同序列时,产生的哈希冲突会有所减少:

在这里插入图片描述
   但负载因此越小,也就意味着空间的利用率越低,此时大量的空间实际上都被浪费了。对于闭散列(开放定址法)来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下,超过0.8会导致在查表时CPU缓存不命中(cache missing)按照指数曲线上升。
   因此,一些采用开放定址法的hash库,如JAVA的系统库限制了负载因子为0.75,当超过该值时,会对哈希表进行增容。

线性探测的优点:实现非常简单。
线性探测的缺点:一旦发生冲突,所有的冲突连在一起,容易产生数据”堆积“,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要多次比较(踩踏效应),导致搜索效率降低。

4.1.2 二次探测

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

Hi=(H0+i^2)%m (i=1,2,3,…)
H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过二次探测后得到的存放位置
m:表的大小

   例如:我们用除留余数法将序列插入到表长为10的哈希表中,当发生哈希冲突时我们采用闭散列的二次探测找到下一个空位置进行插入。
   采用二次探测为产生哈希冲突的数据寻找下一个位置,相比线性探测而言,采用二次探测的哈希表中元素的分布会相对稀疏一些,不容易导致数据堆积。
   和线性探测一样,采用二次探测也需要关注哈希表的负载因子,例如,采用二次探测将上述数据插入到表长为20的哈希表,产生冲突的次数也会有所减少,因此,闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

4.2 开散列——链地址法(拉链法、哈希桶)

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

例如,我们用除留余数法将序列插入到表长为10的哈希表中,当发生哈希冲突时我们采用开散列的形式,将哈希地址相同的元素都链接到同一个哈希桶下。
在这里插入图片描述
   闭散列解决哈希冲突,采用的是一种报复的方式,”我的位置被占用了我就去占用其他位置“。而开散列解决哈希冲突,采用的是一种乐观的方式,”虽然我的位置被占用了,但是没关系,我可以”挂“在这个位置下面。
  与闭散列不同的是,这种将相同哈希地址的元素通过单链表链接起来,然后将链表的头结点存储在哈希表中的方式,不会影响与自己哈希地址不同的元素的增删改查的效率,因此开散列的负载因子相比闭散列而言,可以稍微大一点。
在这里插入图片描述

  • 闭散列的开放定址法,负载因此不能超过1,一般建议控制在[0.0,0.7]之间。
  • 开散列的哈希桶,负载因子可以超过1,一般建议控制在[0.0,1.0]之间

在实际中,开散列的哈希桶结构比闭散列更实用,主要原因有两点:
1.哈希桶的负载因子可以更大,空间利用率高。
2.哈希桶在极端情况下还有可用的解决方案。

哈希桶的极端情况就是,所有元素全部产生冲突,最终都放到了同一个哈希桶中,此时该哈希表增删查改的效率就退化成了O(N)

五、哈希表的闭散列实现

5.1 哈希表的结构

在闭散列的哈希表中,哈希表每个位置除了存储所给数据之外,还应该存储该位置当前的状态,哈希表中每个位置的可能状态:

  1. EMPTY(无数据的空位置)
  2. EXIST(已存储数据)
  3. DELETE(原本有数据,但现在被删除了)

我们可以用枚举定义这三个状态

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
{
public:
//...
private:vector<HashData<K, V>> _tables;size_t _n = 0;     //存储的数据个数
};

5.2 哈希表的插入

向哈希表中插入数据的步骤如下:

1.查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
2.判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
3.将键值对插入哈希表。
4.哈希表中的有效元素个数加一。
其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子大于0.7,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

注意:在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。
将键值对插入哈希表的具体步骤如下:

1.通过哈希函数计算出对应的哈希地址。
2.若产生哈希冲突,则从哈希地址处开始,采用线性探测向后寻找一个状态为EMPTY或DELETE的位置。
3.将键值对插入到该位置,并将该位置的状态设置为EXIST。
注意:产生哈希冲突向后进行探测时,一定会找到一个合适位置进行插入,因为哈希表的负载因子是控制在0.7以下的,也就是说哈希表永远都不会被装满。

bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//负载因子超过0.7就扩容//if((double)_n/(double)_tables.size()>=0.7)//if (_tables.size()==0 || _n * 10 / _tables.size() >= 7)//{//	1、表为空,扩不上去//	2、光扩容无法访问,size没变//	//_tables.reserve(_tables.capacity() * 2); //不能这么做//	size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;//	//_tables.resize(newsize);  //有问题//	vector<HashData> newtables(newsize);//	//遍历旧表,重新映射到新表//	for (auto& data : _tables)//	{//		if (data._state==EXIST)//		{//			//重新算在新表的位置//			size_t i = 1;//			size_t index = hashi;//			while (newtables[hashi]._state == EXIST)//			{//				index = hashi + i;//				index %= newtables.size();//				++i;//			}//			newtables[index]._kv = data._kv;//			newtables[index]._state = EXIST;//		}//	}//	_tables.swap(newtables);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 (auto& data : _tables){if (data._state == EXIST){newht.Insert(data._kv);}}_tables.swap(newht._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;}

5.3 哈希表的查找

在哈希表中查找数据的步骤如下:

1.先判断哈希表的大小是否为0,若为0则查找失败。
2.通过哈希函数计算出对应的哈希地址。
3.从哈希地址处开始,采用线性探测向后进行数据的查找,直接找到待查找的元素判定为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。
注意:在查找过程中,必须找到位置状态为EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了。

HashData<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;}

5.4 哈希表的删除

删除哈希表中的元素非常简单,我们只需要进行伪删除即可,也就是将待删除元素所在位置的状态设置为DELETE。
在哈希表中删除数据的步骤如下:

1.查看哈希表中是否存在该键值的键值对,若不存在则删除失败
2.若存在,则将该键值对所在位置的状态改为DELETE即可。
3.哈希表中的有效元素个数减一。
注意:虽然删除元素时没有将该位置的数据清0,只是将该元素所在状态设为了DELETE,但是并不会造成空间的浪费,因为我们在插入数据时是可以将数据插入到状态为DELETE的位置的,此时插入的数据就会把该数据覆盖。

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

六、哈希表的开散列实现(哈希桶)

6.1 哈希表的结构

   在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点。

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

   与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。
   哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

template<class K,class V,class Hash=HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public://...private:vector<Node*> _tables;   //指针数组size_t _n=0;    //存储有效数据个数};

6.2 哈希表的插入

向哈希表中插入数据的步骤如下:

1.查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
2.判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
3.将键值对插入哈希表。
4.哈希表中的有效元素个数加一。
其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10.
  • 若哈希表的负载因子已经等于1了,则先创建一个新的哈希表,该哈希表大大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

重点:在将原哈希表的数据插入到新哈希表的过程中,不要通过复用插入函数将原哈希表中的数据插入到新哈希表,因为在这个过程中我们需要建立相同数据的结点插入到新哈希表,在插入完毕后还需要将原哈希表中的结点进行释放,多此一举。

实际上,我们只需要遍历原哈希表的每个哈希桶,通过哈希函数将每个哈希桶中的结点重新找到对应位置插入到新哈希表即可,不用进行结点的创建与释放。
说明:为了降低时间复杂度,在增容时取结点都是从单链表的表头开始向后依次取的,在插入结点时也是直接将结点头插到对应单链表。
将键值对插入哈希表的具体步骤如下:

1.通过哈希函数计算出对应的哈希地址。
2.若产生哈希冲突,则直接将该结点头插到对应单链表即可。

bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}Hash hash;//负载因子==1时扩容if (_n == _tables.size()){/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;HashTable<K, V> newht;newht.resize(newsize);for (auto cur : _tables){while (cur){newht.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newht._tables);*/size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtables(newsize, nullptr);//for(Node*& cur:_tables)for (auto& cur : _tables){/*for (size_t i=0;i<_tables.size();++i){Node*& cur = _tables[i];*/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;}

6.3 哈希表的查找

在哈希表中查找数据的步骤如下:

1.先判断哈希表的大小是否为0,若为0则查找失败。
2.通过哈希函数计算出对应的哈希地址。
3.通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可。

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;}

6.4 哈希表的删除

在哈希表中删除数据的步骤如下:

1.通过哈希函数计算出对应的哈希桶编号。
2.遍历对应的哈希桶,存照待删除结点。
3.若找到了待删除结点,则将该结点从单链表中移除并释放。
4.删除结点后,将哈希表中的有效元素个数减一。
注意:不要先调用查找函数判断待删除结点是否存在,这样做如果待删除不在哈希表中那还好,但如果待删除结点在哈希表,那我们需要重新在哈希表中找到该结点并删除,还不如一开始就直接在哈希表中找,找到了就删除。

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;}

七、哈希表的大小为什么建议是素数

  使用除留余数法时,哈希表的大小最好是素数,这样能够减少哈希冲突产生的次数。

八、完整代码

8.1 HashTable.h

#pragma once
#include <vector>namespace OpenAddress
{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{public:bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//负载因子超过0.7就扩容//if((double)_n/(double)_tables.size()>=0.7)//if (_tables.size()==0 || _n * 10 / _tables.size() >= 7)//{//	1、表为空,扩不上去//	2、光扩容无法访问,size没变//	//_tables.reserve(_tables.capacity() * 2); //不能这么做//	size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;//	//_tables.resize(newsize);  //有问题//	vector<HashData> newtables(newsize);//	//遍历旧表,重新映射到新表//	for (auto& data : _tables)//	{//		if (data._state==EXIST)//		{//			//重新算在新表的位置//			size_t i = 1;//			size_t index = hashi;//			while (newtables[hashi]._state == EXIST)//			{//				index = hashi + i;//				index %= newtables.size();//				++i;//			}//			newtables[index]._kv = data._kv;//			newtables[index]._state = EXIST;//		}//	}//	_tables.swap(newtables);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 (auto& data : _tables){if (data._state == EXIST){newht.Insert(data._kv);}}_tables.swap(newht._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;}HashData<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){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;size_t _n = 0;     //存储的数据个数/*HashData* tables;size_t size;size_t _capacity;*/};void TestHashTable1(){int a[] = { 3,33,2,13,5,12,1002 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));if (ht.Find(13)){cout << "13在" << endl;}else{cout << "13不在" << endl;}ht.Erase(13);if (ht.Find(13)){cout << "13在" << endl;}else{cout << "13不在" << endl;}}
}namespace HashBucket
{template<class K,class V>struct HashNode{HashNode<K, V>* _next;pair<K, V> _kv;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>{//BKDRsize_t operator()(const string & s){//return s[0];size_t hash = 0;for (auto ch : s){hash += ch;hash *= 31;}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;}}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;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}Hash hash;//负载因子==1时扩容if (_n == _tables.size()){/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;HashTable<K, V> newht;newht.resize(newsize);for (auto cur : _tables){while (cur){newht.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newht._tables);*/size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtables(newsize, nullptr);//for(Node*& cur:_tables)for (auto& cur : _tables){/*for (size_t i=0;i<_tables.size();++i){Node*& cur = _tables[i];*/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;}size_t MaxBucketSize(){size_t max = 0;for (size_t i=0;i<_tables.size();++i){auto cur = _tables[i];size_t size = 0;while (cur){++size;cur = cur->_next;}printf("[%d]->%d\n", i,size);if (size > max){max = size;}}return max;}private:vector<Node*> _tables;   //指针数组size_t _n=0;    //存储有效数据个数};void TestHashTable1(){int a[] = { 3,33,2,13,5,12,1002 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));ht.Insert(make_pair(25, 25));ht.Insert(make_pair(35, 35));ht.Insert(make_pair(45, 45));}void TestHashTable2(){int a[] = { 3,33,2,13,5,12,1002 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Erase(12);ht.Erase(3);ht.Erase(33);}struct HashStr{//BKDRsize_t operator()(const string& s){//return s[0];size_t hash = 0;for (auto ch : s){hash += ch;hash *= 31;}return hash;}};void TestHashTable3(){//HashTable<string, string, HashStr> ht;HashTable<string, string> ht;ht.Insert(make_pair("sort", "排序"));ht.Insert(make_pair("string", "字符串"));ht.Insert(make_pair("left", "左边"));ht.Insert(make_pair("right", "右边"));ht.Insert(make_pair("", "右边"));HashStr hashstr;cout << hashstr("abcd") << endl;cout << hashstr("bcda") << endl;cout << hashstr("aadd") << endl;cout << hashstr("eat") << endl;cout << hashstr("ate") << endl;}void TestHashTable4(){size_t N = 10000;HashTable<int, int> ht;srand(time(0));for (size_t i = 0; i < N; ++i){size_t x = rand();ht.Insert(make_pair(x, x));}cout << ht.MaxBucketSize() << endl;}
}

8.2 test.cpp


#define _CRT_SECURE_NO_WARNINGS 1#include <iostream>
#include<unordered_set>
#include<unordered_map>
#include<string>
using namespace std;#include "HashTable.h"int main()
{//TestHashTable1();//HashBucket::TestHashTable1();//HashBucket::TestHashTable2();//HashBucket::TestHashTable3();HashBucket::TestHashTable4();return 0;
}

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

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

相关文章

极狐GitLab 线下『 DevOps专家训练营』成都站开班在即

成都机器人创新中心联合极狐(GitLab)隆重推出极狐GitLab DevOps系列认证培训课程。该课程主要面向使用极狐GitLab的DevOps工程师、安全审计人员、系统运维工程师、系统管理员、项目经理或项目管理人员&#xff0c;完成该课程后&#xff0c;学员将达到DevOps的专家级水平&#x…

19.云原生CICD之ArgoCD入门

云原生专栏大纲 文章目录 ArgoCDArgoCD 简介GitOps介绍Argo CD 的工作流程argocd和jinkens对比kustomize介绍ArgoCD和kustomize关系 安装argocdargocd控制台介绍首页应用创建表单SYNC OPTIONS&#xff08;同步选项&#xff09;SYNC POLICY&#xff08;同步策略&#xff09; 应…

【超实用】用Python语言实现定时任务的八个方法,建议收藏!

在日常工作中,我们常常会用到需要周期性执行的任务,一种方式是采用 Linux 系统自带的 crond 结合命令行实现。另外一种方式是直接使用Python。接下来整理的是常见的Python定时任务的八种实现方式。 利用while True: + sleep()实现定时任务 位于 time 模块中的 sleep(secs) 函…

一键完成,批量转换HTML为PDF格式的方法,提升办公效率

在当今数字化的时代&#xff0c;HTML和PDF已经成为两种最常用的文件格式。HTML用于网页内容的展示&#xff0c;而PDF则以其高度的可读性和不依赖于平台的特性&#xff0c;成为文档分享和传播的首选格式。然而&#xff0c;在办公环境中&#xff0c;我们经常需要在这两种格式之间…

openGauss学习笔记-202 openGauss 数据库运维-常见故障定位案例-不同用户查询同表显示数据不同

文章目录 openGauss学习笔记-202 openGauss 数据库运维-常见故障定位案例-不同用户查询同表显示数据不同202.1 不同用户查询同表显示数据不同202.1.1 问题现象202.1.2 原因分析202.1.3 处理办法 openGauss学习笔记-202 openGauss 数据库运维-常见故障定位案例-不同用户查询同表…

LSTM学习笔记

上一篇文章中我们提到&#xff0c;CRNN模型中用于预测特征序列上下文的模块为双向LSTM模块&#xff0c;本篇中就来针对该模块的结构和实现做一些理解。 Bidirectional LSTM模块结构如下图所示&#xff1a; 在Pytorch中&#xff0c;已经集成了LSTM模块&#xff0c;定义如下&…

编译openjdk 调试java

背景 一直很想深入了解java运行机制&#xff0c;想编译debug版本openjdk 实践 安装环境 安装vmware软件&#xff0c;第一步就遇到很多麻烦&#xff0c;找不到免费的vmware。 后来下载了官网的&#xff0c;在github和百度一直搜如何破解&#xff0c;幸亏有大佬传了比较全的…

有道开源RAG引擎 QAnything 版本更新啦

https://github.com/netease-youdao/QAnything 近日&#xff0c;我们将我们的RAG&#xff08;基于检索增强的生成&#xff0c;Retrieval Augmented Generation&#xff09;引擎QAnything开源了&#xff0c;用户可以传入doc, pdf, 图片&#xff0c;ppt, excel 等各种类型的文档…

LLM:RoPE位置编码

论文&#xff1a;https://arxiv.org/pdf/2104.09864.pdf 代码&#xff1a;https://github.com/ZhuiyiTechnology/roformer 发表&#xff1a;2021 绝对位置编码&#xff1a;其常规做法是将位置信息直接加入到输入中&#xff08;在x中注入绝对位置信息&#xff09;。即在计算 q…

uniapp组件库中Collapse 折叠面板 的使用方法

目录 #平台差异说明 #基本使用 #控制面板的初始状态&#xff0c;以及是否可以操作 #自定义样式 #1. 如果修改展开后的内容&#xff1f; #2. 如何自定义标题的样式&#xff1f; #3. 如何修改整个Item的样式&#xff1f; #API #Collapse Props #Collapse Item Props #…

redis-exporter监控部署(k8s内)tensuns专用

reidis-exporter服务需要用到configmap、service、deployment服务 创建存放yaml目录 mkdir /opt/redis-exporter && cd /opt/redis-exporter 编辑yaml配置文件 vi configmap.yaml apiVersion: v1 kind: ConfigMap metadata:name: redis-confnamespace: monitorlab…

【信号与系统】【北京航空航天大学】实验四、幅频、相频响应和傅里叶变换

一、实验目的 1、 掌握利用MATLAB计算系统幅频、相频响应的方法&#xff1b; 2、 掌握使用MATLAB进行傅里叶变换的方法&#xff1b; 3、 掌握使用MATLAB验证傅里叶变换的性质的方法。 二、实验内容 1、 MATLAB代码&#xff1a; >> clear all; >> a [1 3 2]; …

Redis 持久化之 RDB AOF

1、简介 Redis 是一个基于内存的 key-value 类型的 Nosql 数据库&#xff0c;经常用来做缓存操作&#xff0c;但是一旦Redis 宕机&#xff0c;重启之后数据会丢失&#xff0c;因此&#xff0c;需要将内存数据进行持久化&#xff0c;保证服务重启后数据能够恢复之前的状态。Redi…

软件资源管理下载系统全新带勋章功能 + Uniapp前端

测试环境&#xff1a;php7.1。ng1.2&#xff0c;MySQL 5.6 常见问题&#xff1a; 配置好登录后转圈圈&#xff0c;检查环境及伪静态以及后台创建好应用 上传图片不了&#xff0c;检查php拓展fileinfo 以及public文件权限 App个人主页随机背景图&#xff0c;在前端uitl文件…

蓝桥杯练习题dfs与bfs

&#x1f4d1;前言 本文主要是【算法】——dfs与bfs的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#x1f304;每日一句&#xff…

(循环依赖问题)学习spring的第九天

Bean实例的属性填充 Spring在属性注入时 , 分为如下几种情况 : 注入单向对象引用 : 如usersevice里注入userdao , userdao里没有注入其他属性 注入双向对象引用 : 如usersevice里注入userdao , userdao也注入usersevice属性 二 . 着重看循环依赖问题 (搞清原理即可) 问题提出…

Android双击图片放大移动图中双击点到ImageView区域中心,Kotlin

Android双击图片放大移动图中双击点到ImageView区域中心&#xff0c;Kotlin 初始化状态&#xff0c;ImageView里面只是显示一张fitcenter被缩放的原图&#xff0c;当手指在图片上双击后&#xff08;记录双击点位置&#xff1a;mCurX&#xff0c;mCurY&#xff09;画一个红色小圆…

【Linux的权限命令详解】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 shell命令以及运行原理 Linux权限的概念 Linux权限管理 一、什么是权限&#xff1f; 二、权限的本质 三、Linux中的用户 四、linux中文件的权限 4.1、文件访问…

基于SpringBoot Vue高校失物招领系统

大家好✌&#xff01;我是Dwzun。很高兴你能来阅读我&#xff0c;我会陆续更新Java后端、前端、数据库、项目案例等相关知识点总结&#xff0c;还为大家分享优质的实战项目&#xff0c;本人在Java项目开发领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#x…

Django(八)

1. 管理员操作 1.1 添加 from django.shortcuts import render, redirectfrom app01 import models from app01.utils.pagination import Paginationfrom django import forms from django.core.exceptions import ValidationError from app01.utils.bootstrap import BootStr…