数据结构与算法系列15(上)--散列表(哈希表)

什么是散列表

散列表的英文是“Hash Table”,也叫“哈希表”或者“Hash 表”。他是一种根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

散列表的思想

散列表用的是数组支持按照下标进行随机访问的时候,时间复杂度是O(1)的特性。所以散列表其实是数组的一种扩展,可以说没有数组就没有散列表。
我们通过散列函数把元素的键值映射为下标,然后将数据储存在数组对应下标的位置。当我们需要按照键值查找元素时,我们用同样的散列函数,将键值转化为数组下标,从对应的数组下标的位置找元素。

什么是散列函数

散列函数其实就是一个普通的函数,它的主要功能就是要将键值转换为对应的下标,我们可以把它定义为hash(key),其中key表示元素的键值,hash(key)的值就是我们经过散列函数计算得到的下标(散列值)。

如何设计散列函数

1.散列函数计算得到的散列值是一个非负整数。
2.若key1=key2,则hash(key1)=hash(key2)。
3.若key≠key2,则hash(key1)≠hash(key2)。
正是由于第3点要求,所以产生了几乎无法避免的散列冲突问题。

散列冲突的解放方法

1.开放寻址法

思想:
如果出现散列冲突,我们就重新探测出一个空闲的位置,然后将其插入。这里探测出是否有空闲位置的方法我们使用的是“线性探测法”。
使用线性探测法插入数据:
当我们想要往散列表插入数据时,我们先通过散列函数计算出相应的储存位置,如果该位置已经储存有数据了,那我们就从当前位置开始,依次往后面查找,看是否有空闲的位置,直到查找到为止。
使用线性探测法查找数据:
我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素,看是否相等,如果相等,说明就是我们要查找的元素,如果不相等,就顺序往后依次查找。如果遍历到数组的空闲位置还没有找到元素,就说明要查找的元素没有在散列表中。
使用线性探测法删除数据:
为了不让查找算法失效,可以让要删除的数据做一个特殊的标记deleted,当线性探测查找的时候,遇到标记为deleted的空间,我们会继续往下查找而不是停下来。
线性探测法存在的问题
当散列表插入的数据越来越多时,空闲的位置就会越来越少,极端情况下,我们可能要探测完整个散列表,所以最坏情况下的时间复杂度为O(n)。

除了上面讲的通过线性探测法来寻找一个空闲位置,还有另外两种比较经典的探测方法,二次探测和双重探测。
二次探测(Quadratic probing):
线性探测每次探测的步长为1,即在数组中一个一个探测,而二次探测的步长变为原来的平方。
双重散列(Double hashing):
使用一组散列函数,直到找到空闲位置为止。

不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。
装载因子的计算公式是:

散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

2.链表法(更常用)

在散列表中,每一个“桶”,“槽”会对应一条链表,所有散列表相同的元素,我们都把他们放到相同槽位的对应链表中。
使用链表法插入数据:
我们只需要通过散列函数计算出对应的散列槽位, 将其插入到对应的链表中就可以。所以插入数据的时间复杂度是O(1)。
使用链表法查找和删除数据
我们同样通过散列函数计算对应的散列槽位,然后遍历链表查找或者删除。对于散列比较均匀的散列函数,链表的节点个数k=n/m,其中n表示散列表中数据的个数,m表示散列表中槽的个数,所以是时间复杂度为O(k)。

猜你喜欢

转载自blog.csdn.net/qq_34493908/article/details/83957982