前言
观察下列数据
13 24 32 6 4 89 56
在刚开始接触java时,想要在一组序列中查找指定元素,可以遍历这个序列,找到与之匹配的元素,进行返回,他的时间复杂度为O(n)。
对其进行优化可以得到时间复杂度为O(log2n)的方法,即将其存储在一个二叉搜索树中,或者使用二分查找。
4 6 13 24 32 56 89
那么,有没有时间复杂度更低的方法呢?有的,就是哈希。
哈希表:
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
按照这个思想建立的表就为哈希表。
核心
1、内部是一个数组
2、关键字经过变换(hash函数)得到int类型的值
3、int类型的值变成一个合法的下标
4、把关键字放到这个合法的下标的位置
当向该结构中:
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
- 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)。
在了解哈希函数之前,我们需要知道设计哈希函数的目的----降低哈希冲突发生的概率,因此我们需要先了解什么是哈希冲突。
哈希冲突:
对于两个数据元素的关键字i和j,有i!=j,但是有hash(i)=hash(j),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
比如说:
定义一个数组,先往里存放1,3,4,6,7。
定义hash函数为[key%length]得到Index,就可以拿到[key%length]位置的元素了。
但是!!!
如果此时,我们想要往数组中存放11这个数据,而通过我们定义的hash函数处理之后,11应该存放在1下标的位置,这时就会和之前存放的数据产生冲突。
由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率,因此我们可以在冲突发生前对哈希函数进行设计,避免哈希冲突的发生,如果发生了哈希冲突,那么就要想办法解决哈希冲突。
**
冲突的避免
1、设计哈希函数
**
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1 之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数构造方法
1. 直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。 优点:简单、均匀 。 缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况。
2. 除留余数法(最常用的)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
3. 平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。
平方取法比较适合:不知道关键字的分布,而位数又不是很大的情况。
4. 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
5. 随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) =
random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法。
6. 数学分析法
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是
相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现
冲突,还可以对抽取出来的数字进行反转(如1231改成1321)、右环位移(如1231改成1123)、左环移位、前两数与后两数叠加(如1231改成12+31=43)等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。
2、设置负载因子调节
散列表的负载因子定义为:α=填入表中的元素个数/散列表的长度。
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表示填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,表明填入表中的元素越少,产生冲突的可能性就越小。
冲突的解决
1、闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
- 线性探测
比如上面的场景,现在需要插入元素1,先通过哈希函数计算哈希地址,下标为1,因此1理论上应该插在该位置,但是该位置已经放了值为1的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
1、通过哈希函数获取待插入元素在哈希表中的位置
2、如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素1,如果直接删除掉,1查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
- 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨 着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i