哈希表、哈希桶(C++实现)

哈希概念

在顺序结构和平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,O(logN ),搜索的效率取决于搜索过程中元素的比较次数。

而理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找通过该函数可以很快找到该元素。
向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中此位置取元素比较,若关键码相等,则搜索成功。
    该方式为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9}
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
我们将数据映射到capacity为10的哈希表中如下:
在这里插入图片描述
这种方法的搜索速度很快。

哈希函数

常用的2种方法:

1.直接定址法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀
缺点:1需要知道关键字的分布情况,如果给的一组数据范围很大,就会浪费空间
2.不能处理浮点数,字符串等数据

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

优点:数据范围很大
缺点:不同的值映射到同一个位置会冲突

哈希冲突

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

解决哈希冲突

闭散列-开放定址法

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

线性探测

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

Hash(key)=key%len+i(i=0,1,2…)

len:哈希表的大小
如果+0后面有数据那么再+1…直到找到空位置
比如插入11,和1冲突就往后探测。
在这里插入图片描述

动图演示:插入44

在这里插入图片描述
通过上图发现:哈希表中的数据越多,产生哈希冲突的可能性会越大。此时我们引入了负载因子

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

1.负载因子越大,产生冲突的概率越大,增删查改的效率低
2.负载因子越小,产生冲突的概率越小,增删查改的效率高

将哈希表的大小改为20在插入{1,7,6,4,5,9,11,13},产生的冲突变少:
在这里插入图片描述

对于闭散列来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下,超过0.8会导致在查表时CPU缓存不命中按照曲线上升。
线性探测的优点:实现简单
缺点:发生冲突,容易产生数据堆积,出现踩踏,导致搜索效率低。

✨✨✨✨✨✨✨✨✨✨✨✨我是分割线✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

二次探测

为了解决空位置是一个一个找,二次探测避免了这个问题。
二次探测不是再探测一次的意思,二次探测的方法

Hash(key)=key%len+i ^2(i=0,1,2…)

比如插入数据{1,4,5,9,11,13},为了测试我又加了44,54来看看效果:

在这里插入图片描述
比之前的探测好了很多,之前的54是插在44后面的现在到了下表16了,避免了踩踏。

闭散列的实现

哈希表的结构

在闭散列哈希表中不仅要存数据之外,还要存储当前位置的状态,有3种状态:

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

我们用枚举即可

	enum State//标识每个位置的状态
	{
   
		EMPTY,
		EXIST,
		DELETE
	};

为什么要加上状态?

那怎么标识一个空位置呢?用数字标识?例如1:
在这里插入图片描述
但是把11删除后就找不到21了。
在这里插入图片描述
它是和1冲突,从1开始找,1后面是空停止。但是21还在表中,不可能遍历一遍哈希表,这样就是去了哈希表的意义了。因此要在哈希表中加上状态。当哈希表中删除一个元素后,我们不应该简单的将状态设为EMPTY,要将该位置的状态设为DELETE。在查找的过程中,如果当前位置和查找的Key值不相等,但是当前位置的状态是EXIST或者是DELETE,我们都要往后查找,而当我们插入时,可以插入到状态EMPTY和DELETE上。

哈希表中的每个位置的存储结构包括状态和数据

	template<class K, class V>
	struct HashDate
	{
   
		pair<K, V> _kv;//数据
		State _state = EMPTY;//状态
	};

我们还要在哈希表中存入数据的个数来计算负载因子,当负载因子过大就要将哈希表增容。

template<class K, class V>
	class HashTable
	{
     
	public:
	//...
	private:
	    vector<HashDate<K, V>> _table;
		size_t _n = 0;//哈希表中元素的个数
	};

哈希表的插入

插入的逻辑如下:
1.查看是否存在该键值对,如存在则返回false
2.判断哈希表的大小是否需要调整,为0或者是负载因子过大都需要调整
3.插入元素
4.哈希表大小加1

增容的逻辑:
不能原地扩容,那原来数据的映射关系就乱了。我们要新创建一个哈希表对象,是原来的2倍,把旧的数据插入到新表中,在交换2张表即可。

插入键值对的过程:
1.先计算出对应的位置
2.如果发生冲突,进行线性探测处理,向后找到一个状态为EMPTY或者DELETE的位置插入
3.插入到该位置,并将状态设为EXIST

代码如下:

bool insert(const pair<K, V>& kv)
		{
   
			//通过查找看看哈希表中是否存在
			HashDate<K,V>* ret = Find(kv.first);
			if (ret)
			{
   
				//如果存在返回false
				return false;
			}
			if (_table.size() == 0)
			{
   
				_table.resize(10);
			}
			else if ((double)_n /(double)_table.size() > 0.7)
			{
   
				//增容
				HashTable<K,V
  • 62
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 63
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 63
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_End丶断弦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值