【一】哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较,顺序查找时间的复杂度为O(N),平衡树中为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方式:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashfunc)使元素的存储位置和它的关键码之间能够建立一一映射的关系,那么在查找时可以通过该函数直接锁定该元素的位置。
当向该元素中:
插入元素:根据插入元素的关键码,以此函数计算出该元素的存储位置并且按此位置进行存放
搜索函数:对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相同,则搜索成功。
这就是我们所说的哈希(散列)方法,哈希方法中使用的函数被称为哈希(散列)函数,构造出来的结构称为哈希表或者散列表。
用这种存储方式不必进行多次关键码的搜索,所以搜索的速度是很快的。
tips:如果插入两个相同的数据时,会发生什么事?
【二】哈希冲突
定义:不同关键字通过相同哈希函数计算出相同的哈希地址,这种现象被称为哈希冲突或者哈希碰撞,把具有不同关键码而具有相同哈希地址的数据元素称为同义词。那么如果发生哈希冲突如何处理呢?
引起哈希冲突的一个原因可能是:哈希函数设计不是很合理,哈希函数的设计原则:
1.哈希函数的定义域必须包括需要存储的关键码,如果散列表允许有m个地址时,其值域必须在0-m-1之间,哈
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数尽可能按照简单的方式来设计。
常见的几个哈希函数:
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为随机数函数。
关键字不长时均采用此法。
哈希冲突解决:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
所以哈希冲突的两种解决方式:闭散列和开散列
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被填满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突的下一个位置取,那如何取寻找下一个空位置呢?
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,
插入:通过哈希函数获取待插入元素在哈希表中的为止
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
删除:采用闭散列处理哈希冲突时,不能随便删除哈希表中已有的元素,若直接删除元素会影响其它元素的搜索。所以哈希表使用的时候删除使用的标记位删除的方式。
思考:哈希表什么情况下需要进行扩容?如何进行扩容?
散列表的负载因子:填入表中的因子/散列表的长度
负载因子是散列表负载程度的标志性因子,由于表长是定值,a与填入表中的元素个数成正比,所以a越大,填入表中的元素越多,产生冲突的可能性就越大,对于开放定址法,荷载因子是特别重要因素,应严格限制在0. 7-0. 8以下。超过0. 8,查表时的CPU缓存不命中(cache 对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-08以下。超过0.查表时的cpu缓存不命中(缓存)missing)按照指数曲线上升。因此,- - 些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0. 75,超过此值将 失踪)按照指数曲线上升。因此,--些采用开放定址法的散列库,如Java的系统库限制了荷载因子为0.75。
二次探测
线性探测的缺陷是产生冲突的数据堆积在一起,这与其寻找下一个空位置有关系,因为找空位置的方式是挨个向后找,因此二次探测为了避免该问题,找下一个空位置的方法 为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i = 1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。
研究表明:当表的长度为质数且表装载因子不超过0.5时,新的表项一定能够插入,而且任何一个位置不会被探测两次,因此只要表中有一半的空位置,就不会存在表满的问题,在搜索时可以不考虑表装满的情况,但在插入时必须确保表的状态因子不超过0.5,如果超出必须要进行增容,所以比散列的最大缺陷就是空间利用率低,这也是哈希的缺陷
【三】开散列
概念:开散列又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合统称为一个捅,各个捅中的元素通过一个单链表链接起来,各链表的头节点存储在哈希表中。
从上图可以看出,开散列每个捅中存放的都是发生哈希冲突的元素。
开散列的增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端条件下,可能会导致一个桶节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列的最好情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于插入桶的个数时,可以使用哈希表增容。
开散列与闭散列的比较
应用链地址法处理溢出时,需要增设链接指针,似乎增加了存储开销,事实上,由于开地址法需要保持大量的空闲空间以确保搜索效率,如二次探测法要求装载因子<=0.7,而表项所占空间又比指针大很多,所以使用链地址法反而比开地址发节省空间。