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

2024-01-21 05:12

本文主要是介绍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;
}

这篇关于C++进阶--哈希表的的闭散列和开散列(哈希桶)实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/628454

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

哈希leetcode-1

目录 1前言 2.例题  2.1两数之和 2.2判断是否互为字符重排 2.3存在重复元素1 2.4存在重复元素2 2.5字母异位词分组 1前言 哈希表主要是适合于快速查找某个元素(O(1)) 当我们要频繁的查找某个元素,第一哈希表O(1),第二,二分O(log n) 一般可以分为语言自带的容器哈希和用数组模拟的简易哈希。 最简单的比如数组模拟字符存储,只要开26个c

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time