Hash(散列)表
一、优势:
Hash函数可以通过key直接查找并确定值所在位置,而不需要像二叉树那样从根节点开始逐个比较排查。
二、Hash函数构造方法:
1、直接定制法——仅适用于地址大小=关键字的情况
该方法构造的Hash函数为线性函数, H(key)=a*key+b。
例如统计某地区某年龄的人数:
需要查找哪个年龄的人数,就直接找对应的就行。这种哈希函数构造简单,且不会产生哈希冲突(后文会讲),但限制较大。
2、除留余数法——最常用
H(key)=key MOD p (p<=m m为表长)说白了就是除以一个数p得到余数,用余数来作为哈希地址。
那么余数该如何选择呢?
理论研究表明,除留余数法的模p应取不大于表长且最接近表长m素数或是不含20以下的质因子的合数,且p最好取1.1n~1.7n之间的一个素数(n为存在的数据元素个数)。例如:当n=7时,p最好取11、13等素数
举个例子:
有7、22、18、30、39、24等几个数,表长为9,取p=7,则7MOD7=0,7就放在0的位置上;22MOD7=1,22就放在1的位置上;以此类推,有冲突就先后移。
3、数字分析法——数字位数大、且有规律可循(事先知道数据分布)
举个例子——同村同年龄人员的身份证号。同村同年的话,他们的身份证的前面数位都是相同的,就可以用后面不同的来存储。
例如:H(key)=key%100000
4、平方取中位数法——数据位数小(事先不知道数据分布)
将数据平方(平方的目的是为了扩大差异),再取中位数作为哈希地址。
例如:key=1356 平方得到1838736,可取387作为地址;
key=2431 平方得到5909761,可取097作为地址。
5、折叠法——数字位数大(事先不知道数据分布)
例如:key=123 456 789,可以存储在6 15 24 取其后三位524作为hash地址。
三、Hash冲突解决方法:
1、开放定址法(再散列法):
基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。这种方法有一个通用的再散列函数形式:
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)为哈希函数,m 为表长,di称为增量序列。增量序列的取值方式不同,相应的再散列方式也不同。
主要有以下三种:
a、线性探测再散列:
dii=1,2,3,…,m-1
其特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
b、二次探测再散列
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
其特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
c、伪随机探测再散列
di=伪随机数序列。
具体实现时应建立一个伪随机数发生器,eg.(i=(i+p)%m),并给出一个随机数作为起点。
例子:(引自 http://blog.csdn.net/tanggao1314/article/details/51457585)
已知哈希表长度m=11,哈希函数为:H(key)= key % 11,则H(47)=3,H(26)=4,H(60)=5,假设下一个关键字为69,则H(69)=3,与47冲突。
1.
线性探测再散列处理冲突,下一个哈希地址为H1=(3 + 1)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 + 2)% 11 = 5,还是冲突,继续找下一个哈希地址为H3=(3 + 3)% 11 = 6,此时不再冲突,将69填入5号单元。
2.
二次探测再散列处理冲突,下一个哈希地址为H1=(3 + 12)% 11 = 4,仍然冲突,再找下一个哈希地址为H2=(3 - 12)% 11 = 2,此时不再冲突,将69填入2号单元)。
3.
伪随机探测再散列处理冲突,且伪随机数序列为:2,5,9,………,则下一个哈希地址为H1=(3 + 2)% 11 = 5,仍然冲突,再找下一个哈希地址为H2=(3 + 5)% 11 = 8,此时不再冲突,将69填入8号单元。
2、再哈希法——同时构造多个不同的哈希函数:
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
3、链地址法:
基本思想:将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
4、建立溢出表:
基本思想:将Hash表分为基本表和溢出表两部分,在基本表中发生冲突的元素都放入溢出表中。
四、Hash表的查找
1、查找过程和造表过程一致,假设采用开放定址法处理冲突,则查找过程为:
对于给定的key,计算hash地址index = H(key)
如果数组arr【index】的值为空 则查找不成功;
如果数组arr【index】== key 则查找成功;
否则,使用冲突解决方法求下一个地址,直到arr【index】== key或者 arr【index】==null。
2、查找效率限制——ASL(平均查找长度)
其影响因素有:
选用的hash函数;
选用的处理冲突的方法;
hash表的饱和度,即装载因子 α=n/m(n表示实际装载数据长度 m为表长)。
一般情况,假设hash函数是均匀的,则在讨论ASL时可以不考虑其它的因素,hash表的ASL是处理冲突方法和装载因子的函数。
前人已经证明,查找成功时如下结果:
可以看到无论哪个函数,装载因子越大,平均查找长度越大,那么装载因子α越小越好?也不是,就像100的表长只存一个数据,α是小了,但是空间利用率不高啊,这里就是时间空间的取舍问题了。通常情况下,认为α=0.75是时间空间综合利用效率最高的情况。
上面的这个表可是特别有用的。假设我现在有10个数据,想使用链地址法解决冲突,并要求平均查找长度<2
那么有1+α/2 <2
α<2
即 n/m<2 (n=10)
m>10/2
m>5 即采用链地址法,使得平均查找长度< 2 那么m>5
各种树的平均查找长度,是基于存储数据n的函数,但hash表不同,他是基于装载因子的函数,也就是说,当数据n增加时,我可以通过增加表长m,以维持装载因子不变,确保ASL不变。
那么hash表的构造应该是这样的:
(参考自https://blog.csdn.net/u011109881/article/details/80379505)
五、hash表的删除
链地址法是可以直接删除元素的;
但是开放定址法是不行的,拿前面的双探测再散列来说,假如我们删除了元素1,将其位置置空,那 23就永远找不到了。正确做法应该是删除之后置入一个原来不存在的数据,比如-1。