1.概念
散列表用的是数组支持按照下标随机访问的特性,所以散列表其实就是数组的一种扩展,由数组演化而来
key
: key.
散列函数(hash函数)
:把key转化为hash值的函数
散列值(hash值)
:数组的下标.
hash = hash_function(key)
对hash函数的基本要求:
-
散列函数计算得到的散列值是一个非负数整数:对于数组的下标,从0开始
-
如果 key1 == key2 那么 hash(key1) == hash(key2)
-
如果 key1 != key2 那么 hash(key1) != hash(key2)
对于第三点,其实无法完全避免,满足这一点的hash函数几乎不存在。当 key1 != key2时, hash(key1) 有可能等于 hash(key2), 尤其当数组的空间有限,这中可能性就更大。这种情况较 hash冲突。
再好的hash函数也无法避免hash冲突的问题。解决hash冲突有两个思路:
- 开放寻址法(Open addressing)
- 链表法(chaining)
2.实现
2.1 开放寻址法
核心思路:如果出现了散列冲突,就重新探测一个空闲的位置,将其插入。如何重新探寻新的位置?一个比较简单的探测方法,线性探测
:如果某个数据经过散列之后,存储位置已经被占用了,就从当前位置开始,依次往后查找,直达找到空闲位置为止。
上图中,橙色表示 已经有元素,黄色表示该位置空闲。hash之后,得到位置为7。此时7已经有元素,因此顺序往下,8,9都有元素,从头开始,0,1,直至找到x.
-
插入
按之前线性探方法直至找到空闲位置。时间复杂度为: 最好:下一个元素有空位 O(1) 最坏:上一个相邻位置才有空位,需要遍历n-1次 -> O(n) 平均:(1+2+3+。。。+n-1)/(n-1) = n/2 -> O(n)
-
删除
如果直接把找到的元素置空,则有问题,因为无法知道是否真正删除了元素。可以把这个元素标记为为"deleted"状态。当遍历至这个元素时,如果key一样,状态已经标记为deleted,则说明已经删除。如果无找不到,则须一次遍历查找,然后删除
-
查询
先通过hash查找,O(1). 如果找到hash(key),比较key,如果key一只,则结束。如果key不一致,则通过线性查找,直至找到,平均为O(n)。如果该位置空闲,则说明不在数组中。
其他开放寻址方法:
-
二次探测
步长不为1,而是为原来2次方,即: hash(key) + 0, hash(key)+1, hash(key)+2, ... 变为 hash(key) + 2^0, hash(key)+2^1, hash(key)+2^2, ...
-
双重散列
使用一组hash函数,而非一个hash函数:{hash1, hash2, hash3, hash4, ...}, 当hash1(key)已经有值,则用hash2(key), 依次类推,直至找到空闲位置.
不管采用哪种探测方法,当散列中空闲位置不多时,散列冲突的概率就会大大提高。为了尽可能保证散列列表的操作效率,一般情况下,我们会尽力可能保证散列中有一定比例的空闲槽位。我们用装载因子(load factor) 来表示空位的多少
装载因子 = 填入表中的元素个数 / 散列表的长度
装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
-
2.2 链表法
链表法是一种更加常用的散列冲突解决办法,相对开放寻址法,它要简单很多。如下图。在散列中,每个”桶(bucket)“或则”槽(slot)“ 会对应一条链表,hash(key)相同的元素都会放入相同槽位对应的链条中:
时间复杂度:
对于插入和删除,
- 最好: 数组每个位置都直接插入,无需链表,为O(1)
- 最坏:每次hash(key)的值都一样,此时,变成了一条链表,复杂度为O(n)
- 平均:均匀分布,假设有m个桶,则每条链的长度为 n/m, 所有复杂度为 O(k) = O(n/m)