C++之哈希详解

本文深入探讨了哈希的概念、哈希冲突及其解决方案,包括闭散列和开散列方法。详细介绍了闭散列的线性探测和二次探测,以及开散列的链地址法。此外,文章还提供了哈希表的插入、查找和删除操作的实现细节,强调了负载因子在决定何时扩容中的重要性,并讨论了如何处理字符串键的哈希问题。
摘要由CSDN通过智能技术生成

哈希

目录

哈希

哈希的概念

哈希冲突

常见哈希函数:

哈希冲突的解决:

闭散列

线性探测的缺陷是产生冲突的数据堆积在一块,相比线性探测的好处,如果一个位置有很多值映射,冲突剧烈,那么它们存储时相对会比较分散,不会引发一片一片的冲突

开散列

闭散列的实现

数据的存储结构

哈希表结构

哈希表的插入

哈希的查找

哈希的删除

哈希闭散列的完整代码

开散列

开散列的概念

数据的存储结构

哈希表的结构

哈希表的插入

哈希表的查找

哈希表的删除

哈希桶的析构函数

开散列完整代码


哈希的概念

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

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

当向该哈希结构中:

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

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

那么,例如我们有数据{1,3,7,4,8};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

用该方法可以避免对关键字的比较,可以达到快速搜索。但是,如果我们此时再插入44,44%10得到的同样是4,但是位置已经被占用,所以引出了以下的问题:

哈希冲突

 对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过
相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?

常见哈希函数:

直接定址法

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

除留余数法

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

哈希冲突的解决:

闭散列

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

方法:

线性探测:index+i (i = 1,2,3,4…)

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

二次探测:index+i^2(i = 1,2,3,4…)

线性探测的缺陷是产生冲突的数据堆积在一块,相比线性探测的好处,如果一个位置有很多值映射,冲突剧烈,那么它们存储时相对会比较分散,不会引发一片一片的冲突

开散列

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

闭散列的实现

当我们实现哈希表时,数据是怎么存储呢?数据的结构又是什么呢?数据的存储很好想到,可以用一个vector线性表存就好了,那么数据的结构除了存储数据还需要什么呢?比如我们有以下数据:

 当我们查找一个值时,我们通过哈希函数求得位置,如果对应位置不是,那么很可能是遇到了冲突,所以我们需要继续往后查找,遇到空位置时说明这个值是不存在的,因为如果存在,肯定存储在了第一个空位置,那么当我们删除一个值,直接删除的话这个位置变成了空,但是这样有问题:使得查找一个值提早遇到空,那么就找不到这个值。

就比如我们删除333,当我们查找14时,14%10=4,我们锁定到了4的位置,但是由于删除333后,4的位置是空的,那么就会形成一个14存在,却无法查找的到的局面,

所以我们需要通过枚举来引入三个状态,每个位置存储值得同时再存储一个状态标记:空、满、删除。

三种状态:

enum Status
{
	EXIST,//存在
	EMPTY,//空
	DELETE//删除
};

数据的存储结构

我们使用KV模型

template<class K,class V>
struct HashData
{
	Status _status;//当前状态
	pair<K, V> _kv;
};

哈希表结构

vector用来存储HashData,_n则用来记录表中的有效数据个数:

template<class K,class V>
class HashTable
{
public:

private:
	vecor<HashData<K, V>> _tables;
	size_t _n=0;
};

哈希表的插入

我们为了避免哈希冲突变多,我们引入一个负载因子,当负载因子过大时需要对哈希表进行增容。

  1. 如果哈希表中已经存在该键值对,则插入失败返回false
  2. 判断该哈希表的大小以及负载因子,确定是否需要增容
  3. 将键值对插入哈希表
  4. ++_n

当我们使用线性探测时,会发现产生冲突的数据会堆积在一起,连续位置比较多,会引发踩踏洪水效应,所以在这里我们使用了二次探测。

我们解决增容的问题,首先我们确定的是_table.size() == 0时需要增容,其次我们设置负载因子=有效数据/表的大小,如果负载因子大于0.7时就增容。那么如何增容呢&

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值