C++哈希

1,哈希表

1.1哈希概念

哈希(hash)又叫"散列",是一种组织数据的方法。本质就是通过哈希函数把关键字key和存储位置建立一个映射关系,查找时通过这个哈希函数计算出key的存储位置进行快速查找。

可以举一个简单的例子,对'a'~'z'进行哈希映射建立一个哈希表,那么此时可以通过利用ASCII码表来建立映射,比如对于'a',ASCII码值为97,那么映射可以是97-'a'那么对应便是下标为0的位置。依次类推得到往后的所有映射。

那么所谓的哈希函数便是hash(x)=x-97 对应的key便是'a'--'z' 。当然由此可以看出,上述的哈希表其实也是一个**"数组"**。

那么此时可以看出所谓的key值若不是整数,则可以通过一定手段将其转化为整数再映射,那么这也是对于key的要求----必须可以转化为整型。

那么上述所使用的可以总结成"直接定址法"。

1.2直接定址法

直接定址法就如同上述所示的,每个映射都是通过关键字直接计算出一个绝对或相对的位置。当关键字的范围比较集中时,直接定址法是非常简单高效的。

但不可避免的就是会产生哈希冲突。

1.3哈希冲突

哈希冲突就是关于两个不同的key也可能映射到同一个位置的现象。

实际场景是:当数据范围在0,9999的N个值,映射到M个空间的数组中去(M>=N),那么需要借助哈希函数完成映射,可以假定哈希函数h(key)=key%M,那么在一定范围内所得的必然有重复的结果,此时现象就称为哈希冲突。

哈希冲突不可避免,那么关于哈希冲突已经产出了一系列解决方案。

1.4负载因子

负载因子表示着一个哈希表的空间利用率和哈希冲突的概率。

假设哈希表已经存储了N个值,哈希表的大小为M,负载因子=N/M。

负载因子越大,表示哈希冲突概率越大,空间利用率越高,那么根据此也可以得出一个解决哈希冲突的方向:当负载因子到达一定大小时及时扩容以降低哈希冲突概率。

(我感觉这里的思想和插入vector的空间占比达到一定数量时就扩容有异曲同工之妙)

1.5哈希函数

一个理想的哈希函数应该是让N个关键字被等概率的均匀的分配到哈希表的M个空间,但实际上哈希冲突难以避免,只能尽量选择合适的方法映射设计出较为好的哈希函数。

1.5.1除法散列法/除留余数法

哈希函数h(key)=key%M,假设哈希表的空间为M个,通过key除以M的余数作为映射位置的下标。

对于该方法需要注意M的取值,建议取不太接近2的整数次幂的一个质数(素数),当M取2的幂或10的幂时,总是更容易出现哈希冲突(如2^x,那么此时的h(key)就相当于保留key的后x位,那么对于后x位相同的值,计算的结果也就相同了)。

但实践中总是有各种各样的应用和场景,也不能把话说的太死~

1.5.2乘法散列法

哈希函数h(key)=floor(M*((A*key)%1.0)),其中floor表示对表达式进行向下取整,A=(0,1),关于A的值,普遍公认黄金分割点比较好A=((√5 - 1)/2)=0.618033......,

乘法散列法的思路是:用关键字key乘上常数A,并抽出其小数部分,再用M乘以小数部分,向下取整。

该方法对于M没有要求。

1.5.3全域散列法

哈希函数h(key)=((a*key+b)%P)%M,P是一个足够大的质数,a是1,p-1之间随机一个整数,b是0,p-1之间随机一个整数,这些函数构成了一个p*(p-1)组全域散列函数组。

每次初始化哈希表时,都会随机分配函数组中的一个散列函数使用,后续就会固定该函数不会再改变。这种情况就大大增加了散列函数的随机性,也拥有了更高的安全性和加密性。

以上就是常见的哈希函数以及方法,实践中一般选择除法散列法作为哈希函数,但无论什么方法都避免不了哈希冲突, 所以针对哈希冲突时,需要有专门的解决方法。

1.6处理哈希冲突

1.6.1开放定址法

在开放定址法中,当出现哈希冲突,则会按照某种规则找到一个没有存储数据的位置再存储,那么对于负载因子必然的较小的。

这里对应的规则有三种:线性探测,二次探测,双重探测。

线性探测

从发生冲突的位置开始,依次线性向后探测,直到找到没有存储数据的位置。

若h(key)=key%M,若hash0冲突,则线性探测公式为:

**hc(key,i)=hashi=(hash0+i)%M, i={1,2,3...,M-1},**负载因子一定小于1,则最多探测M-1次一定能找到。

线性探测有明显弊端,容易占用还未计算映射的数据的位置,而导致后续数据都出现抢占的问题,效率在一定程度上降低,所以也就有了二次探测。

二次探测

从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个空位置。若向右为表末尾,则跳到表头,向左同理。

若h(key)=key%M,若hash0冲突,则二次探测公式为

hc(key,i)=hashi=(hash0 i^2)%M, i={1,2,3...,M/2},

当hashi<0时,需要hashi+=M。

双重散列

该方法运用两个哈希函数,当第一个哈希函数计算发生冲突时,使用第二个哈希函数计算出一个与key相关的偏移量,不断往后探测直到找到空位置。但并不是特别常用。

若h1(key)=key%M,若hash0冲突,则双重探测公式为

hc(key,i)=hashi=(hash0 + i*h2(key))%M, i={1,2,3...,M/2},

**要求h2(key)<M,且h2(key)与M互为质数,**有两种简单的取值方法:

a.当M为2的整数幂时,h2(key)从0,M-1任选一个奇数;

b.当M为质数时,h2(key)=key%(M-1)+1;

1.6.2链地址法

开放定址法中所有元素都放在哈希表里,链地址法中所有数据不再直接存放在哈希表中,而是通过链表形式挂起,哈希表中只存储一个指针,这样的方法比开放地址法少一步计算,更加方便高效。

连地址法也叫哈希桶。

2,unordered_set和unordered_map简介

unordered_set和unordered_map都是STL已经封装好的容器,底层就是哈希表(哈希桶)。

那么两者和map,set的区别在于"unordered",unordered_map和unordered_set是无序存储,通过哈希函数计算"key"的存储位置,平均查找为O(1),非常高效,而map和set底层是红黑树的有序存储。

2.1unordered_set

对于unordered_set的使用基本和set没有太大差别,不过多赘述。只叙述差异:

1,对key的要求不同。set对于key要求支持小于比较。unordered_set要求key支持转成整型且支持等于比较。对于其要求不同,可以理解为应用场景的不同,在set中的红黑树要求有序插入,所以只要求可以比较。在unordered_set中的哈希表是通过哈希函数来计算key的位置,且需要解决哈希冲突等问题,所以需要转成整型计算,且要求支持等于。

**2,迭代器差异。**set的迭代器是双向迭代器,unordered_set是单向迭代器。set底层是红黑树,也就是二叉搜索树,所以底层迭代器是走中序便有序迭代,且去重。unordered_set底层是哈希表,那么迭代器遍历是无序,且同样去重。

**3,性能差异。**绝大多数的情况unordered_set的效率快于set。红黑树的增删查改效率为O(logN),而哈希表的效率是O(1)。

2.2unordered_map

对于unordered_map和map的差异,与上述的set和unordered_set差异没有太大区别,不过多赘述。

1,对key的要求不同。map对于key要求支持小于比较。unordered_map要求key支持转成整型且支持等于比较。

2,迭代器差异。map的迭代器是双向迭代器,unordered_map是单向迭代器。

**3,性能差异。**绝大多数的情况unordered_map的效率快于map。红黑树的增删查改效率为O(logN),而哈希表的效率是O(1)。

相关推荐
世事如云有卷舒1 小时前
GoogTest测试框架
c++·测试
hy.z_7772 小时前
【C++】9. Vector
开发语言·c++
wabs6662 小时前
关于哈希表【力扣454.四数相加II的思考】
数据结构·算法·leetcode·散列表
我能坚持多久2 小时前
优选算法——专题一双指针(上):附四道例题详解
c++·学习·算法
Tim_102 小时前
【C++】023、移动语义&深拷贝
开发语言·c++·算法
ShineWinsu2 小时前
对于C++中unordered_map的详细介绍
数据结构·c++·算法·面试·stl·哈希表·unordered_map
东东最爱敲键盘3 小时前
day3.c++函数与预处理 string
c++·算法
djjjx.3 小时前
【 C++ 】stack、queue、优先级队列、仿函数、容器适配器
开发语言·c++
lingran__3 小时前
C++ 高阶数据结构:AVL 树万字详解|原理、四种旋转、key‑value 实现、调试校验、set/map/红黑树 底层前置知识
数据结构·c++·二叉搜索树·平衡二叉树·avl树·红黑树前置