哈希表主要是提供常数时间的基本操作。它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。在哈希表中,散列函数实现地址映射,但是由于存在冲突问题,所以,又必须考虑散列冲突的处理。
散列函数:负责将某一元素映射为“大小可接受之索引”,这样的函数称为散列函数。
散列函数的构造方法
如上图所示,散列函数的构造方法较多,下面就几种有代表性的进行总结。
直接定址法
直接定址法的散列函数符合如下形式:
f
(
k
e
y
)
=
a
∗
k
e
y
+
b
;
f(key) = a*key + b;
f(key)=a∗key+b;
其中,a、b均为常数。最常见的直接定址法是数组。
优点:简单、均匀、无冲突。
应用场景:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。例如数组就可以视作一个直接定址法的例子,其a = 1,b = 1.
除法散列法
f
(
k
e
y
)
=
k
/
p
(
p
<
=
m
)
f(key) = k/p (p <= m)
f(key)=k/p(p<=m)
由于只需要做一次除法操作,所以除法散列法是非常快的;但是如果p的取值不好,那么可能导致不同元素映射到同一个表中。
平方取中法
将数字平方,然后再取中间三位作为散列地址,再比如关键字是 1234 那么它的平方就是 1522756 ,抽取中间 3 位就是 227 用作散列地址。
处理散列冲突
使用散列函数可能带来问题是:不同元素被映射到相同的位置。这无法避免,这就是碰撞问题。主要有以下几种方式进行解决。
线性探测
负载系数:元素个数除以表格大小。负载系数永远在0~1之间,如果大于1,说明元素太多,表格已经存储不下了。
当散列函数计算元素的插入位置,而该位置上的空间已不再可用,那么就往下寻找,直到找到一个可用空间为止。
进行元素搜索时,如果计算出来的位置上元素于我们的搜寻目标不符,那么就往下寻找,直到找到吻合者,或者遇上空格元素。
元素删除,采用惰性删除,也就是只标记删除记号,实际删除则等待表格重新整理时再进行,这是因为哈希表中每一个元素都关系到其他元素排列。
线性探测存在一个问题:平均插入成本的成长幅度,远高于复杂系数的成长幅度。
线性探测法的公式如下:
简单的说,就是f(key) mod m,结果产生碰撞,那么我们使用(f(key) + di) mod m,其中di是一个常数,我们使用这种方式,直到找到一个不会冲突的地址为止。
二次探测
二次探测解决碰撞问题的方程式为:F(i) = i2。
如果散列函数计算出来新元素的位置为 H,而该位置已被使用,那么就依次尝试 H+12、H+22…。
二次探测的公式如下:
我们可以发现,二次探测与线性探测十分类似,唯一区别在于其使用的di是平方数,这种方式可以避免关键字集中在同一区域。
开链
每一个表格元素维护一个 list:散列函数为我们分配一个 list,然后我们在那个 list 身上执行元素的插入、搜寻、删除等操作,虽然针对 list 进行的搜寻是一种线性操作,但是如果 list 够短,那么速度还是够快。
那么开链如果碰到地址冲突会怎么样呢?开链是这样处理的,如果发现key不同而f(key)相同,那么只需要在同一子链表中添加一个节点即可。
链地址法虽然能不产生冲突,但是也带来了查找时需要遍历单链表的性能消耗。
参考博客:https://blog.csdn.net/csdnnews/article/details/110944316
代码链接:https://github.com/KevinCoders/MyStudy/blob/master/datastructure/hashtable.cpp