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)。