一、数组和链表的弊端
1、对于所有可能的关键字集合U,如果用数组来存储U中的关键字,则需要分配的存储单元至少等于U中关键字的个数。这种方法的弊端很明显,实际中U可能很大,计算机的内存毕竟有限,用数组去存储这么大一个集合不切实际
2、对于链表来说也是同样的道理,对每个关键字需要分配一个节点来存储,这样空间上就会受到限制
3、还有,实际出现的关键字集合K有可能远远小于U,这样用数组或链表来存储整个全集U,就会有大部分元素(节点)没有被使用到,那么这些空间就是严重的浪费
4、最后,数组和链表在查找插入删除操作上各有不足。
比如,数组的查找效率非常高,时间复杂度为O(1),而它的插入和删除操作就非常麻烦
链表刚好相反,插入删除方便,查找则需要O(N)的时间复杂度
二、散列表的思想
把全集U的每一个关键字通过一个散列函数(Hash Function)h映射到表T{0, 1, ... , m-1}的某个下标,表T{0, 1, ... , m-1}可以简单的理解为有m个元素的数组 h:U—>{0, 1, ... , m-1}
我们用散列表的一个关键目的就是为了节省存储空间,所以一般情况下m要远远小于|U|。
这里就有一个问题,两个不同的关键字通过h计算可能会得到同一个哈希值(函数值),例如上图的k2 != k5,但h(k2) == h(k5),我们称这种情形为冲突或碰撞(collision)。
我们选择散列函数h的时候,要尽可能地减少冲突次数。要做到这一点,就需要散列函数h尽可能的"随机",即函数值分布尽可能均匀,不要集中在某个区间。
同时也要明白,把一个大集合U硬塞到一个小数组T{0, 1, ... , m-1},是不可能避免冲突的。
那么,解决冲突主要有两种方法,一是链接法,二是开放地址法,这里着重介绍第一种。
三、通过链接法解决冲突
在链接法中,我们把映射到同一下标的关键字存储在一个链表中,数组中保存指向该链表的指针(有可能为空)。
装载因子α=N / m,N为所有关键字的个数,m为数组的元素个数。α表示的是每个链表中平均存储的元素个数。
显然,链接法的最坏情形是所有关键字都映射到一个下标上,这样所有关键字都被存储到一个链表中,其性能退化为和链表,还得加上散列函数h的开销。
所以,链接法的性能依赖于散列函数的选择,我们要选择尽量使关键字均匀分布在数组的各个元素上的散列函数。
四、散列函数
1、除法散列法
通过取k除以m的余数,将关键字k映射到含有m个元素的数组的一个下标上。
h(k) = k % m
一个不太接近2的整数幂的素数,通常是m的一个较好的选择
2、乘法散列法
构造散列函数的乘法散列法包含两个步骤:
①用关键字k乘上常数A(0<A<1),并提取kA的小数部分
②用m乘以这个小数部分,再向下取整
h(k) = floor(kA % 1)
A = (√5 -1) / 2 ≈ 0.618是个比较理想的值
开放地址法是