散列表
散列表是普通数组概念的推广,由下标与值的映射提高为键值分别与下标的映射。
数组中查找元素需对下标遍历来查询值,复杂度通常是非常数。而散列表由键到下标是常数级,因此散列表是典型的空间换时间的数据结构。
由键到下标的映射有不同的方法,即哈希函数,可能会造成哈希冲突。
一、直接寻址表
当全域比较小时,关键字即为下标,因此全域中关键字的个数与表的长度大小相等。其中键值对存储在表中,而没有值的关键字在表中打上阴影。
direct-address table,记为T[0,,m-1],其中每个位置称为槽,对应全域中的一个关键字。查找、插入、删除的复杂度均为1。
二、散列表
当全域很大时,直接寻址表的长度就会变得很大。而且当键值对的数目远小于全域中关键字的个数时,表中的大部分阴影空间将被浪费掉。
因此引入散列表的结构,不再是一一映射。哈希函数可能将不同的关键字映射到表中的同一个槽,造成哈希冲突,因此一般有效槽的数目
小于等于键值对的数目。
(原文)理想的解决方法时避免所有的冲突。我们可以试图选择一个合适的散列函数h来做到这一点。一个想法就是使h尽可能的随机,从而避免冲突或者使冲突的次数最小化。
实际上,术语散列原意就是随机混杂与拼凑,即体现了这种思想。一个散列函数必须是确定的,因为一个给定的输入k必须得到唯一的h(k)。我们需精心设计散列函数来尽量减少
哈希冲突的次数,此外需要有解决哈希冲突出现后的办法。
通过链接法解决冲突 将散列到同一槽的所有元素放入一个链表中。槽中存放该链表的表头。
链接法散列的分析 记存放n个元素的、具有m个槽位的散列表的(load factor)装载因子a=n/m,即一个链的平均存储元素数。装载因子可以小于、等于、或大于1。
最坏情况下,所有的元素都被映射到同一个槽位,此时退化成链表,查找的复杂度为n。散列表的性能取决于散列函数将关键字分布在槽位上的均匀程度。
假设任何一个给定元素等可能地散列到m个槽位中的任何一个,且与其他元素被散列到什么位置上无关,为简单均匀散列,其查找的平均时间为1+a,包括关键字到槽位的1和链表的平均长度a(装载因子)。
三、散列函数
一个好的散列函数应满足简单均匀散列假设:每个关键字都被等可能地散列到m个槽位中的任何一个,并与其他关键字已散列到哪个槽位无关。如果关键字不是自然数,就需要找到一种方法将其转换为自然数。
除法散列法 通过取k处以m的余数,将关键字k映射到m个槽位中的一个,即散列函数为:h(k) = k mod m。这里m不是随便取的,例如m不应为2的幂,如果m为2的p次方,则h(k)就是k(二进制形式)的p个最低位数除非已知各种最低p位的排列形式为等可能的,否则在设计散列函数时,最好考虑关键字的所有位。
乘法散列法 全域散列法 好复杂 不写了。
散列函数的最坏情况是将所有的关键字映射到同一个槽位中,退化成链表。改进方法是随机的选择散列函数,使之独立于要存储的关键字,称为全域散列。从一组精心设计的散列函数中,聪明的随机选择函数,可以确保每一个操作序列都具有良好的平均情况运行时间。
设计一个全域散列函数类 不写了
四、开放寻址法
所有的元素都存放在散列表里,每个槽都存放元素或NIL。当查找某个元素时,要系统地检查所有的表项,直到找到该元素。没有链表,因此没有元素存放在散列表外。散列表可能被填满,以至于不能插入新的元素。装载因子绝对不会超过1。不像链接法要存放指针,开放寻址法节省的空间可以用来提供更多的槽位,潜在地减少了冲突。为了解决哈希冲突,可以将关键字进行多次哈希运算,直到槽位为空,将元素插入该槽位。在查找元素时,依旧进行多次哈希运算,直到槽位为空,说明表中不存在查找元素。
给定装载因子为p=n/m<1的开放寻址散列表,并假设是均匀散列的,则一次不成功查找,其期望的探查次数至多为1/(1-p);向表中插入一个元素至多需要1/(1-p)次探查;一个成功查找中的探查期望数至多为(1/p)*(1/(1-p))。表中的每个关键字被查找的可能性是相同的。
以上只是通俗的说明,具体的探查序列更为复杂。
五、完全散列