Hash
函数的确定
通过前面学习到, Hash
表的查询效率并不是 O(1)
,它与 Hash
函数、散列冲突等因素有关。如果 Hash
函数确定得不好,可能导致散列冲突概率升高,查询效率下降。那么,该如何设计 Hash
函数呢?
首先, Hash
函数一般设计得不要过于复杂,过于复杂的 Hash
函数会导致计算时间过多,从而影响散列表的性能;
其次, Hash
函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,而且即使出现冲突,散列到每个槽中的数据也会比较平均,不会导致某些槽中的数据过多,而另一部分槽中的数据过少的情况。
传统的 Hash
函数的设计方法有直接寻址法、平方取中法、折叠法、随机数法等,也可以根据实际情况自己设计 Hash
函数。
装载因子的确定
为了定量的表示 Hash
表中空位的多少,定义装载因子:
Hash表的装载因子 = 填入表中的元素个数 / Hash表的长度
由公式可知,装载因子越大,说明 Hash
表中的元素越多,空闲位置越少,散列冲突的概率越大,散列表的性能就会下降。
对于没有频繁插入和删除的静态数据结合来说,可以根据数据的特点和分布情况设计出符合这些数据的 Hash
函数,从而减少了散列冲突。
但是大部分情况下是动态数据,数据集合是频繁变动的,我们无法事先知道数据的个数,因此也无法事先申请一个足够大的 Hash
表。随着数据加入,填入表中的元素个数增多,装载因子增大,当装载因子达到一定程度时,散列冲突便不可接受,因此我们无法根据数据的特征和分布情况设计出符合这些数据的 Hash
函数,而是需要动态扩容,重新申请一个更大的 Hash
表,将数据重新存储到新的 Hash
表中。
如下图中的散列表,当装载因子达到0.8时进行扩容,装载因子变为0.4,原来的数据就会存储在新的 Hash
表中。
当数据插入到 Hash
表时,如果装载因子还未达到临界值,此时还不需要扩容,插入的数据非常快,但如果装载因子达到了临界值,这是就需要先进行扩容,然后再插入数据,这个时候就会变得很慢。
当数据需要从 Hash
表中删除时,如果 Hash
表已经经历过扩容,随着数据的删除,空闲空间会越来越多。当程序对内存空间非常敏感时,可以设置当装载因子小于某个临界值时,启动动态缩容,让内容空间得到充分利用;当程序对内存空间不太敏感时,就不需要进行动态缩容处理。
动态扩容策略
为了减少动态扩容耗时,我们可以将扩容的操作穿插在插入操作过程中。具体如下图所示:
这样每次插入时迁移一个数据,没有集中一次性迁移数据那样耗时,不会形成明显的阻塞。
由于迁移过程中,有新旧两个 Hash
表,查找数据时,先在新的 Hash
表中进行查找,如果没有,再去旧的 Hash
表中进行查找。