如果两个数据项被散列映射到同一个槽,需要一个系统化的方法在散列表中保存第二个数据项,这个过程称为冲突。前边提到,如果说散列函数是完美的,那么就不会有散列冲突,但完美散列函数常常是不现实的,解决完美散列冲突成为散列方法中很重要的一部分。
1.1 线性探测解决散列的一种方法就是为冲突的数据项再找一个开放的空槽来保存。最简单的就是从冲突的空槽开始往后扫描,直到碰到一个空槽。如果到散列表尾部还没有找到,就从首部接着扫描。这种寻找空槽的技术称为开放定址法,向后逐个槽寻找的方法就是开放定址技术中的线性探测。
我们接着之前的例子把44、、55、、20逐个插入到散列表中,h(44)=0,但发现0#槽已经被77占据了,向后找到第一个空槽1#,保存;h(55)=0,但发现0#槽已经被77占据了,向后找到第一个空槽2#,保存;h(20)=9,但发现9#槽已经被31占据了,向后,在从头开始找到3#保存。
线性探测法的一个缺点是有聚集的趋势,也就是说如果同一个冲突的数据项较多的话,这些数据项就会在槽附近聚集起来,从而导致连锁式影响其他数据的插入。为了避免聚集的一种方法就是将线性探测扩展,从逐个探测改为跳跃式探测。
把44、、55、、20插入到散列表中,下图展示了采用“加3”探测策略处理冲突后的元素分布情况:
1.2 再散列再散列泛指在发生冲突后寻找另一个槽的过程。采用线性探测时,再散列函数是newhashvalue = rehash(oldhashvalue),并且rehash(pos)=(pos+1)%sizeoftable。“加3”探测策略的再散列函数可以定义为rehash(pos) = (pos+3)%sizeoftable。也就是说,可以将再散列函数定义为rehash(pos) = (pos+skip)%sizeoftable。注意:“跨步”(skip)的大小要保证表中所有的槽最终都被访问到,否则就会浪费槽资源。要保证这一点常常建议散列表的大小为素数。
1.3 平方探测不再固定skip的值,而是逐步增加skip值,如1、3、5、7、9,这样槽号就会是原散列值以平方数增加。
将44、、55、、20插入到散列表中:
1.4 数据项链除了寻找空槽的开放定址技术之外,另一种解决散列冲突的方案是将容纳单个数据项的槽扩展为容纳数据项集合(或者对数据项链表的引用)。这样散列表中的每个槽就可以容纳多个数据项,如果有散列冲突发生,只需要简单地将数据项添加到数据项集合中。查找数据项时则需要查找同一个槽中的整个集合,当然,随着散列冲突的增加,对数据项的查找时间也会相应增加。
采用链接发处理冲突:
02实现映射抽象数据类型
字典是Python最有用的数据类型之一,字典是一种可以保存键值对的数据类型,其中关键码可用于查询关联的数据值,这种键值关联的方法称为“映射”。
映射的结构是键-值关联的无序集合,关键码具有唯一性,通过关键码可以唯一确定一个数据值。
映射定义的操作如下:
Map():创建一个空的映射,返回一个空的映射集合。
put(key,va1):往映射中加入一个新的键值对。如果键已经存在,就用新值替换旧值。
get(key):返回key对应的值,如果key不存在,则返回None。
de1:通过de1 map[key]这样的语句从映射中删除键值对。
len():返回映射中存储的键值对的的数目。
in:通过key in map这样的的语句,在键存在时返回True,负责返回False。
使用字典的优势在于给定关键码能够很快得到关联的数据值。为了达到快速查找的目标,需要一个支持高效查找的实现方案,可以采用列表数据结构加顺序查找或者二分查找。当然更合适的是使用前述的散列表来实现,这是因为散列搜索算法的时间复杂度可以达到O(1)。
下面,我们用一个HashTable类来实现映射抽象数据类型,该类包含了两个列表作为成员,其中一个s1ot列表用于存储键;名为data的列表用于存储值。
在slot列表查找到一个键的位置后,在data列表对应相同位置的数据项即为关联数据。
保存键的列表就作为散列表来处理,这样可以迅速查找到指定的键。注意散列表的大小,虽然可以是任意数,但考虑到要让冲突解决算法能有效工作,应该选择为素数。
HashTable类的构造方法:
hashfunction方法采用了简单求余方法来实现散列函数,而冲突解决则采用线性探测“加1”再散列函数。put函数假设,除非键已经在self.slots中,负责总是可以分配到一个空槽。该函数计算初始的散列值,如果对用的槽中已有元素,就循环运行rehash函数,直到遇见一个空槽。如果槽中已有这个键,就用新值替换旧值。
Put函数:
同理,get函数也先进计算初始散列值,如果值不在初始散列值对应的槽中,就使用rehash确定下一个位置。
HashTable类的最后两个方法提供了额外的字典功能。我们重载__getitem__和__setitem__,以通过[]进行访问。这意味着创建HashTable类之后就可以熟悉的索引运算符了。
get函数:
下边来看看运行情况。首创建一个散列表并插入一些元素,接下来访问并修改散列表中的某些元素。其中,键是整数,值是字符串。
03散列算法分析
散列在最好的情况下,可以提供O(1)常数级时间复杂度的查找性能。由于散列冲突的存在,查找比较次数就没有这么简单。
评估散列冲突的最重要信息就是负载因子λ ,一般来说:
如果λ 较小,散列冲突的几率就小,数据项通常会保存在其所属的散列槽中;如果λ 较大,意味着散列表填充较满,冲突会越来越多,冲突解决也越复杂,也就需要更多的比较来找到空槽。如果采用数据链的话,意味着每条链上的数据项增多。
如果采用线性探测的开放定址法来解决冲突(λ 在0-1之间):
如果采用数据链来解决冲突(λ 可大于1):