一、散列函数
散列是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。建立了关键字与存储位置的映射关系,公式如下:
存储位置 =f(关键字)
设所有可能出现的关键字集合记为U(简称全集),实际发生(即实际存储)的关键字集合记为K(|K|比|U|小得多)。
散列方法是使用函数f将U映射到表T[0..m-1]的下标上(m=O(|U|))。这样以U中关键字为自变量,以f为函数的运算结果就是相应结点的存储地址。从而达到在O(1)时间内就可完成查找。
其中:
- f:U(可能出现的全集)→{0,1,2,…,m-1} ,通常称f为散列函数(Hash Function)。散列函数f的作用是压缩待处理的下标范围,使待处理的|U|个值减少到m个值,从而降低空间开销。
- T为散列表(Hash Table)。
- f(Ki)(Ki∈U)是关键字为Ki结点存储地址(亦称散列值或散列地址)。
- 将结点按其关键字的散列地址存储到散列表中的过程称为散列(Hashing)
二、直接寻址法
散列的概念属于查找,它不以关键字的比较为基本操作,采用直接寻址技术。
在最坏情况下,散列表的查找和删除的最坏时间代价为O(n),但是在实际中,散列表的性能是很好的,在一些合理的假设下,散列表的查找和删除操作的平均时间代价为O(1)。
在说直接寻址法之前,先举个例子:
现在有10个门,并且从1到10标好了号码,你手中有5号门的钥匙,那如何打开第五号门呢?肯定不是一个一个的去试,而是直接去开第5号门。
python的列表中也有通过key找到数据的思想,列表是可以直接访问的,通过列表的起始地址加上一个偏移量得到目标数据的存储地址。
list_a = [1,2,3,4]
list_a[2]
>>>3
这种方法是很快的,可以在O(1)时间内完成。直接寻址表就借助了列表这种可直接访问的优势。
如果我们现在要对0-100岁的人口数字统计表,那么我们对年龄这个关键字就可以直接用年龄的数字作为地址。此时f(key) = key。
地址 | 年龄 | 人数 |
00 | 0 | 500万 |
01 | 1 | 600万 |
02 | 2 | 450万 |
... | ... | ... |
20 | 20 | 1500万 |
... | ... | ... |
这个时候,我们可以得出这么个哈希函数:
f(0) = 0,f(1) = 1,……,f(20) = 20。
这个是根据我们自己设定的直接定址来的。人数我们可以不管,我们关心的是如何通过关键字找到地址。
如果我们现在要统计的是80后出生年份的人口数,那么我们对出生年份这个关键字可以用年份减去1980来作为地址。此时f (key) = key-1980。
地址 | 出生年份 | 人数 |
00 | 1980 | 500万 |
01 | 1981 | 600万 |
02 | 1982 | 450万 |
... | ... | ... |
20 | 2000 | 1500万 |
... | ... | ... |
假如今年是2000年,那么1980年出生的人就是20岁了,此时 f(2000) = 2000 - 1980,可以找得到地址20,地址20里保存了数据“人数500万”。
也就是说,我们可以取关键字的某个线性函数值为散列地址,即:
f(key) = a × key + b
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中,直接寻址法虽然简单,但却并不常用。
三、除留余数法
除留余数法此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:
f( key ) = key mod p ( p ≤ m )
mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。
一个例子
-
很显然,本方法的关键就在于选择合适的p, p如果选得不好,就可能会容易产生同义词。下面我们来举个例子看看:
有一个关键字,它有12个记录,现在我们要针对它设计一个散列表。如果采用除留余数法,那么可以先尝试将散列函数设计为如下方法:
f(key) = key mod 12
比如29 mod 12 = 5,所以它存储在下标为5的位置。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
关键字 | 12 | 25 | 38 | 15 | 16 | 29 | 78 | 67 | 56 | 21 | 22 | 47 |
不过这也是存在冲突的可能的,因为12 = 2×6 = 3×4。如果关键字中有像18(3×6)、30(5×6)、42(7×6)等数字,它们的余数都为6,这就和78所对应的下标位置冲突了。
甚至极端一些,对于下图的关键字,如果我们让p为12的话,就可能出现下面的情况,所有的关键字都得到了0这个地址数,这未免也太糟糕了点。
下标 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
关键字 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 |
但是我们如果不选用p=12来做除留余数法,而选用p=11,则结果如下:
下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 0 | 1 |
关键字 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 |
这个时候就只有12和144有冲突,相对来说,就要好很多了。
如何合理选取p值
使用除留余数法的一个经验是,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
这句话怎么理解呢?再举个例子:某散列表的长度为100,散列函数H(k)=k%P,则P通常情况下最好选择哪个呢?
A、91 B、93 C、97 D、99
实践证明,当P取小于哈希表长的最大质数时,产生的哈希函数较好。所以选97,因为它是离长度值最近的最大质数。