目录
平衡二叉树的学习中,学习及模拟实现了AVL树和红黑树,得益于其结构,查找效率可以达到惊人的 O ( l o g N ) O(logN) O(logN),但是平衡树中调平衡的开销及学习的成本也是不低的。于是今天再来学习一个同样高效,甚至更优的哈希表(桶),其实现难度也没有AVL树和红黑树高;而且 unordered系列的关联式容器的底层就是采用了哈希结构,unordered系列的关联式容器比inordered系列的关联式容器(map,set,multi…)效率甚至更优;之所以效率比较高,是因为其底层使用了哈希结构。
哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O ( l o g 2 N ) O(log_2 N) O(log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(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之间- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常用哈希函数——除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
- p一般为表长,即==m
哈希函数设计的核心就是尽量减少不同值经哈希函数计算后映射在同一哈希地址上,由此基础再尽量符合人们的认知即可。
- 注意:哈希函数的设计只能降低哈希冲突发生的可能,但是无法避免哈希冲突
- 借助负载因子,减少哈希冲突。
负载因子
定义:负载因子是指哈希表中已存储元素数量与哈希表总容量之间的比率。
计算公式:负载因子 λ = (已存储元素数量) / (哈希表总容量)。
性能影响:
- 当负载因子较低时,哈希表相对空闲,有较多的空闲槽位可供使用,这有助于减少哈希冲突,提高查找和插入操作的效率。
- 当负载因子较高时,哈希表的槽位大部分被占用,这可能会导致哈希冲突的增加,进而影响哈希表的性能。
空间利用率:
- 负载因子越高,空间利用率越高,但可能牺牲一定的性能。
- 负载因子越低,空间利用率越低,但性能可能更优。
同时,负载因子也可以是哈希表(桶)扩容的判断标志。不同实现方式的负载因子设定也不同。
哈希冲突的解决
对于哈希冲突这一问题,常用的解决方案有以下两种。
闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置呢?
方案一:线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
还是上述情况,此时再插入一个44,由于哈希地址为4的已经有元素了,此时采用线性探测,