直接散列就是按照数组的方式存储,一个记录的关键字就当做它的索引,这种方式对有些情况无疑是不适合的会造成极大的浪费。
散列表,主要是将一个关键字映射或者说散列到h(x)的位置(x为关键字),要力图避免碰撞,这就需要选择合适的散列函数,选择散列函数的一个主导思想是:让h(x)尽可能地随机,这样可以最小化碰撞。
但有时全域空间太大,因此无法避免地要发生碰撞情况,首先介绍解决碰撞情况的链接法。链接法就是将所有散列到同一个槽中的元素连接成一个链表。注意将一个元素插入槽位对应的链表中时,采用的头插法,即一个新元素插入到头节点的下一个位置,这样新插入的节点就在链表的后部而后插入的元素就在链表的前部。
一个包含m的槽位可以存放n个元素的散列表的装载因子为a=n/m,即一个槽位平均存放的元素数。对于查找来说,如果查找不成功,那么对于映射到的槽位一定是查找到了链表的尾端,时间为:o(1+a),a为每个槽位对应链表的平均长度,同样对于查找成功来说它的时间不大于查找不成功的期望时间所以时间为<=o(1+a),对于删除来说,由于是单链表,删除操作也和查找操作类似。当元素数量和槽位个数成一定比例时,则n=o(m),因此n/m=a=常数,所以对于o(1+a)=o(1),也就是对于链接法而言查找、插入和删除操作的平均时间均为o(1)。
与链接法相对的是开放寻址法,它将所有的元素都存放在散列表中而不使用链表,例如当h(k)的槽位已经被占时,那么它将寻找另外一个空的槽位。开放寻址法的特点是不利于删除,所以在有删除的应用中往往采用链接法。开放寻址主要包括三种:线性探查,二次探查和双重探查。
线性探查,就是当h(k)已经满时,那么接着看h(k)+1的位置即下一个槽位....+2的位置.....一直将散列表绕了一圈,这容易造成群集的现象,即连续被占用的槽位不断增加,平均查找时间也不断增加,因为当一个空槽位前有i的满槽位时,那么该槽位被占的概率将是(i+1)/m,这样连续不断增加的连续满槽位将给查找造成较高的开销。
二次探查,二次探查和线性探查类似,不过当遇到满槽时不是寻找下一个空位,而是加一个偏移量(即偏移量非1),此偏移量依赖于二次项,h(k,i)=(h'(k)+a*i+b*i2)mod m,i=0,1...m-1,a,b为辅助常数,即初始时为h(k,0),如果遇到满槽位,那么下次探查h(k,1)即增加偏移量为a+b,若还是满槽位那么探查h(k,2)增加偏移量为a*2+b*4。。。。这样不断下去直到遇到一个空槽位,这样的方式比每次增加的偏移量均为1的线性探查要好,但也会造成轻度的二次群集现象,而且如果两个关键字的初始探查槽位是一样的,那么他们的整个探查序列都将一样,因为当初始探查槽位满后接下来的探查都将不依赖于关键字而仅依赖于偏移量(依赖于一个二次项)。
双重散列,就是利用两个散列函数来决定最终位置,h(k,i)=(h1(k)+i*h2(k)) mod m,这种方法用了更多的探查序列。
完全散列,就是采用链接法(连接的不是一个链表而是数组)的基础上,在每个链接的数组中上再采用一个散列函数来定位而不是简单的放入。