散列(hash)又称哈希,是一种组织数据的方式。从其中文译名来看,有打乱排列的意味,事实上也正是这样。散列函数能够将一组数据随机排布到某个输出区间上,这种随机性使得我们可以借其实现更高性能的容器数据结构。
本文从基本的散列表(hash table)开始,介绍链接法和开放寻址法对冲突的不同解决方案,进一步引出全域散列(universal hashing)与完全散列(perfect hashing)的概念与实现方式,并推导它们的时间和空间复杂度。最后,看看实际中,Java是如何实现散列表的。
散列表的由来
考虑某一类数据,如果关键字的取值范围是有限的,比如所有由三位十进制数字组成的整数,其取值范围为0~999。那么我们可以直接创建一个大小为1000的数组,将每个元素保存在相同下标对应的位置上,需要访问哪个直接通过下标索引,时间复杂度为
这种方法的问题是,虽然有1000种可能的数据,但实际上总的数据量可能很小,也许只有10个,它们在数组中分布的很稀疏,相当于浪费了大量的存储空间。
此时,散列的作用就凸显出来了。我们可以使用某个散列函数将关键字的定义域
大多数情况下,至少会有两个数据被映射到了同一个位置,这种情况我们称为冲突。解决冲突的方式有两种,链接法和开放寻址法,我们将分别阐述这两种方法。
链接法
链接法在每个数组中链接一条链表,将冲突的元素依次放置在链表中。访问时,先根据散列函数计算数组的下标,然后遍历对应的链表,找到目标元素。下图展示了链接法的存储结构。
其中,
散列表的原理很简单,但问题是这种数据结构的性能如何呢?
可以想象,最好的情况下,散列函数将关键字均匀地分布在整个数组上,此时,每个链表的平均长度很短。最坏情况下,散列函数将所有关键字恰好都映射到同一个下标,链表长度与数据总数相同,导致时间复杂度为
这样看来,散列表的性能比较依赖于散列函数的选取。我们将最好情况归纳为一个假设,称为“简单均匀散列”。该假设认为我们选择的散列函数可以将任何一个元素等可能地散列到
我们将
接下来的问题是如何选取散列函数,使其尽量接近简单均匀散列的假设。
对于前面提到的将