注:本文如涉及到代码,均经过Python 3.7实际运行检验,保证其严谨性。
本文阅读时间约为6分钟。
前面说过,如果两个数据项被散列映射到同一个槽,需要一个系统化的方法在散列表中保存第二个数据项,这个过程被称为“解决冲突”。
如果散列函数是完美的,那就不会有散列冲突,但实际情况是,完美散列函数常常并不存在,解决散列冲突成为散列方法中很重要的一部分。
解决散列的一种方法就是,为冲突的数据项再找一个开放的空槽来保存。最简单的就是从冲突的槽开始往后扫描,直到碰到一个空槽。如果到散列表尾部还未找到,则从首部接着扫描。
这种寻找空槽的技术被称为“开放定址(open addressing)”。
向后逐个槽寻找的方法则是开放定址技术中的“线性探测(linear probing)”。
线性探测Linear Probing
还是以前面说过的这一组数据来作为示例说明线性探测具体如何操作。
假设有下列数据项:
54, 26, 93, 17, 77, 31
通过求余法我们得到了其散列值及对应槽号如下图Pic-512-1的上半部分所示。
现在,我们要做的是,把44、55、20三个数据项逐个插入到该散列表中。具体过程如下:
h(44)=0,但发现0#槽已被77占据,向后(向右)找到第一个空槽#1,把44保存在1#槽中。
h(55)=0,因为0#槽、1#槽均已被占据,后面的2#槽是空的,因此把55保存在2#槽中。
h(20)=9,发现9#槽已被31占据,向右,10#也被54占据,再从头扫描,0#、1#、2#等槽均已被占据,后面的3#槽是空的,因此把20保存在3#槽中。
整个过程如上图Pic-512-1所示。
上述方法就是开放定址里的线性探测法。
需要注意的是,如果采用线性探测方法来解决散列冲突的话,那么散列表的查找也应该遵循同样的规则。
如果在散列位置没有找到查找项的话,就必须向后做顺序查找,直到找到查找项或者碰到空槽(即查找失败)。
线性探测的改进
线性探测法有一个缺点,就是有聚集的趋势,即:如果同一个槽冲突的数据项较多的话,这些数据项就会在槽附近聚集起来,从而连锁式影响其他数据项的插入。
为了避免这种不利的聚集趋势,一种方法就是将线性探测扩展,从逐个探测改为跳跃式探测。比如,以+3的方式探测插入44、55、20。
还是用线性探测的例子来说明跳跃式探测是具体如何操作的。
还是假设有下列数据项:
54, 26, 93, 17, 77, 31
我们现在要把44、55、20三个数据项跳跃式(指定以+3的间隔)插入到该散列表中。具体过程如下:
h(44)=0,但发现0#槽已被77占据,向后+3个槽,找到#3槽。#3槽是空槽,可以存放数据,于是把44保存在3#槽中。
h(55)=0,因为0#槽,向后+3个槽,找到#3槽,已有数据项44;继续向后+3个槽,找到#6槽,已有数据项17在里面;继续向后+3个槽,找到#9槽,依然有数据项31占据着;继续向右+3个槽,到了#1槽。#1槽为空,可以存放数据项,于是把55保存在#1槽中。
h(20)=9,发现9#槽已被31占据,向右+3个槽,找到1#槽,不为空;继续向右+3个槽,找到#4,有26在里面;继续向右+3个槽,#7槽为空,可以存放数据项,于是把20存放到#7槽中。
如下图Pic-512-2所示:
冲突解决方案:再散列rehashing
重新寻找空槽的过程可以用一个更为通用的“再散列rehashing”来概括:
newhashvalue = rehash(oldhashvalue)
对于线性探测来说,
rehash(pos) = (pos + 1) % sizeoftable
对于“+3”的跳跃式探测则是:
rehash(pos) = (pos + 3) % sizeoftable
跳跃式探测的再散列通式是:
rehash(pos) = (pos + skip) % sizeoftable
这里要注意的是,skip的取值不能被散列表大小整除,否则会产生周期,造成很多空槽永远无法探测到的后果。如果把散列表的大小设为质数(如11),则可以避免这种情况。
除了将线性探测改善为跳跃式探测,还能将其变为“二次探测(quadratic probing)”。
二次探测是什么意思呢?就是对于
rehash(pos) = (pos + skip) % sizeoftable
来说,skip的值不再是固定的某个值,而是逐步增加的,如1、3、5、7、9等。
这样槽号就会是原散列值以平方数增加:
h, h+1, h+4, h+9, h+16...
这也是一种能令散列值分散的好办法。
冲突解决方案:数据项链Chaining
除了寻找空槽的开放定址技术之外,另一种解决散列冲突的方案是,将容纳单个数据项的槽扩展为容纳数据项集合(或者对数据项链表的引用)。
这样,散列表中的每个槽就可以容纳多个数据项,如果有散列冲突发生,只需要简单地将数据项添加到数据项集合中。
查找数据项时则需要查找同一个槽中的整个集合。在同一个集合中查找,就用顺序查找法。当然,随着散列冲突的增加,对数据项的查找时间也会相应增加。
还是拿那组数据为例:
54, 26, 93, 17, 77, 31
要求插入44、55、20三个数据项。
如用数据项链的方法,每个槽都可以容纳一个数据项的集合。操作示意如下图Pic-512-3所示:
To be continued.