散列表也是一种符号表,主要特征是可以将键通过散列函数映射为一个数组索引,然后利用这个数组索引就可以做很多东西。

散列函数

当我们输入一个对象,不论这是个什么东西,经过散列函数处理之后输出一个0到M-1的范围之内的整数。
《算法》-查找[散列表]
对于散列函数有一些要求:

  1. 相等的对象(使用equals()函数)的散列值是相同的
    2.同样的散列值不同的两个对象不相等
    3.在输出范围之内尽量均匀分布

下面介绍两种实现散列表的方式,分别基于拉链发和线性探测法。

基于拉链法的散列表(SeparateChaining)

假设键的数目为N,数组大小为M,一般对于拉链法,N是大于M的。我们将某个键散列到0到M-1中的一个数,那么随着键的数目的增加,两个键之间一定会有重复的索引,这就发生了所谓的碰撞冲突,拉链法解决碰撞冲突的方法就是每个数组位置保存一个链表的引用,每个新加入的键先找到数组的位置,然后插入对应的链表。查找的时候同样的,先对要查找的键进行散列,然后到相应位置的链表中查找。对于拉链法,每个链表的平均长度为N/M,那么可以看出他比一个无序链表或者数组的性能提高了M倍。看着下面的图应该很好理解。
《算法》-查找[散列表]

基于线性探测法的散列表(LinearProbing)

对于线性探测法,数组容量是大于键的数量的,并且在后面可以看到,数组不能太满,否则影响性能。主要思想是,我们维护两个数组,一个是键的数组,一个是值得数组,当我们将一个键散列到数组中的时候,如果当前位置是空的,那么就直接插入,如果已经有了元素,那么就往下一个位置插入,如果还是被占了,那就继续,直到找到一个空位置,然后再插入。查找的时候也是一样,根据键散列的位置我们去查找,如果当前位置的键和要查找的键不相同,那么就继续往后查找,要么找到,要么又碰到空的位置,那么此时就是查找未命中。看着下面的图,就能对这个过程有着清楚地了解。
《算法》-查找[散列表]

删除

线性探测法的一个重要的操作是删除,但是删除不能仅仅将某个键置为null,因为这样如果它后面本来还有的键就可能因为这个null键而访问不到,我们的做法是将这个置为null之后直到下一个null键之间的数据重新加入散列表。代码见后面的delete()方法。

调整大小

对于线性探测甚至拉链法,我们都需要调整数组大小来保证性能。对于线性探测法,我们需要新建一个LinearProbingHashST()对象,只是新建对象的时候要扩大容量,然后把当前对象的数据重新put()进新的对象里面,最后把新对象的两个数组的引用传给当前数组。

参考:
《算法4》散列表

相关文章: