hashtable概述
hashtable是一种以空间换时间的数据结构,哈希函数让hashtable在插入、删除、搜索等操作上有“常数平均时间”的表现,因此很受欢迎。
hashtable的冲突解决
使用hash function会带来一个问题:可能有不同的元素被映射到相同的位置。有几种解决办法,我们一一介绍。
一. 线性探测(linear——probing)
当hash function计算出某个元素的插入位置,而该位置不可用(有其他元素),该怎么办? 简单的办法是循序往下一一寻找。只要表格够大,总能找到容身之处。
使用这种办法,在搜索的时候,如果hashfunction计算的位置与我们搜索的值不符合,就循序一一往下寻找,直到遇上空格元素。至于元素的删除,则必须使用惰性删除(lazy deletion),也就是只标记删除记号(墓碑),实际删除操作则等到表格重新整理的时候再进行——这是因为hash table中的每一个元素不仅表述它自己,也关系到其他元素的排列。
但这种方法问题很大:
1. 这种方法下,最坏的情况是遍历了整个哈希表,平均情况则是寻访一半的元素,这和我们期待是常数时间的复杂度相去甚远。
2. 主集团问题(primary clustering):如果插入的数据,对某些位置有明显的偏向性,就会造成一些位置人满为患,而另一些却永远都用不到。随着插入数据的增多,这种现象会越来越严重并大大增加插入的时间复杂度,造成空间的浪费。
二. 二次探测(quadratic probing)
二次探测主要用来解决主集团问题。这种方案的策略是如果hash function计算的新元素位置为H,但H已经被使用,那么就尝试新位置H+1^2, H+2^2,..., H+i^2。
二次探测方法给我们带来的新的疑问:
1. 循序访问的线性探测能确保下一个位置一定是不同的,那二次探测法能不能保证计算出来的下一个位置一定与当前位置不同(避免陷入死循环、重复计算)?
2. 二次探测法的计算开销比线性大,是否值得?
3. 当负载系数过高(即表格已经快被填满了)时,表格能否动态增长?
幸运的是,只要假设表格大小为质数(prime),而且永远保持负载系数在0.5以下(也就是说超过了0.5就重新配置表格大小),那么就可以确定每插入一个新元素所需要的探测次数不多于2。
至于计算开销问题,线性探测法所需要的是一个加法(加1)、一个测试(看是否需要绕转回头)、以及一个偶需为之的减法(用以绕转回头)。而二次探测需要的则是一个加法(从i-1到i)、一个乘法(计算i^2)、一个mod运算。然而我们可以优化,
Hi = H0 + i^2(mod M)
Hi-1 = H0 + (i-1)^2(mod M)
整理得, Hi - Hi-1 = i^2 - (i - 1)^2(mod M) ---->> Hi = Hi-1 + (2i-1)(mod M)
因此我们简化用前一个H的值来计算下一个H,不需要计算二次方。因为乘法是乘以2,可以直接使用位移位快速完成。至于mod,也可以证明并非真正需要。
最后一个问题是哈希表的成长。欲扩充表格,首先必须找出下一个新的而且足够大(大2倍)的质数,然后考虑重新建立表格(rehashing)的开销——不能原封不动的拷贝,我们必须验证旧表格中的每一个元素,计算其在新表格中的位置,然后再插入到新表格中。
二次探测法经过优化,已经值得关注了,但二次探测法仍然有次集团(secondary clustering)问题。如果两个元素经过hash function计算出来的位置相同,则插入时所探测的位置也相同,形成某种浪费。解决次级集团的办法有有,如复式散列(double hashing)(如果计算结果相同,则使用下一个哈希函