散列表 又叫 哈希表 (hash table)。通过访问key而直接访问存储的value值。它的key - value之间存在一个映射函数,我们可以通过key值和“看不到”的映射函数(散列函数)访问对应的value值。这加快了查找的速度!存放记录的数组称做散列表。散列方法不同于顺序查找、二分查找、二叉排序树及B-树上的查找。它不以关键字的比较为基本操作,采用直接寻址技术 (就是说,它是直接通过key映射[映射函数,实现的方式有多种] 到内存地址上去的)。在理想情况下,无须任何比较就可以找到待查关键字,查找的期望时间为O(1)(面试的时候犯了这个错误)。
举个形象的例子来说:为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名x到首字母F(x)的一个函数关系),在首字母为W的表中查找“王”姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中散列函数的函数法则F(),存放首字母的表对应散列表。关键字和函数法则理论上可以任意确定。[维基百科]
散列表
设所有可能出现的关键字集合记为U(简称全集)。实际发生(即实际存储)的关键字集合记为K(|K|比|U|小得多)。
散列方法是使用函数h将U映射到表T[0…m-1]的下标上(m=O(|U|))。这样以U中关键字为自变量,以h为函数的运算结果就是相应结点的存储地址。从而达到在O(1)时间内就可完成查找。
其中:
① h:U→{0,1,2,…,m-1} ,通常称h为散列函数(Hash Function)。散列函数h的作用是压缩待处理的下标范围,使待处理的|U|个值减少到m个值,从而降低空间开销。
② T为散列表(Hash Table)。
③ h(Ki)(Ki∈U)是关键字为Ki结点存储地址(亦称散列值或散列地址)。
④ 将结点按其关键字的散列地址存储到散列表中的过程称为散列(Hashing)
我们基本知道了value和key之间存在一个映射关系h,简单的表示为 value = h(key)。 我们会有疑问,函数以允许多对一出现的,也就是说,当多个key对应一个value时怎么办?
这就是散列表中的冲突。
散列表的冲突现象
(1)冲突
两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。发生冲突的两个关键字称为该散列函数的同义词(Synonym)。
【例】上图中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的结点的存储地址相同。
(2)安全避免冲突的条件
最理想的解决冲突的方法是安全避免冲突。要做到这一点必须满足两个条件:
①其一是|U|≤m
②其二是选择合适的散列函数。
这只适用于|U|较小,且关键字均事先已知的情况,此时经过精心设计散列函数h有可能完全避免冲突。
(3)冲突不可能完全避免
通常情况下,h是一个压缩映像。虽然|K|≤m,但|U|>m,故无论怎样设计h,也不可能完全避免冲突。因此,只能在设计h时尽可能使冲突最少。同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中。
通常有两类方法处理碰撞:开放寻址(Open Addressing)法和链接(Chaining)法。前者是将所有结点均存放在散列表T[0…m-1]中;后者通常是把散列到同一槽中的所有元素放在一个链表中,而将此链表的头指针放在散列表T[0…m-1]中。
(1)开放寻址法
所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。
<1>线性探测
给定一个普通的散列函数h’:U —>{0,1,…,m-1},线性探测方法采用的散列函数为:h(k,i) = (h’(k)+i)mod m,i=0,1,…,m-1
探测时从i=0开始,首先探查T[h'(k)],然后依次探测T[h'(k)+1],…,直到T[h'(k)+m-1],此后又循环到T[0],T[1],…,直到探测到T[h'(k)-1]为止。探测过程终止于三种情况:
(1)若当前探测的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探测的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探测到T[h’(k)-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
线性探测方法较容易实现,但是存在一次群集问题,即连续被占用的槽的序列变的越来越长。采用例子进行说明线性探测过程,已知一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:
散列过程如下图所示:
<2>二次探测
二次探测法的探查序列是:h(k,i) =(h’(k)+i*i)%m ,0≤i≤m-1 。初次的探测位置为T[h’(k)],后序的探测位置在次基础上加一个偏移量,该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间。
<3>双重散列
该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许多特性。采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数。初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m。
(2)链接法
将所有关键字为同义词的结点链接在同一个链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0…m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。
举例说明链接法的执行过程,设有一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:
最终结果如下图所示:
构造散列函数
(直接复制的维基百科,详细可参见[1]) 散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快定位。
直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即hash(k)=k或hash(k)=a\cdot k + b,其中a,b为常数(这种散列函数叫做自身函数)
数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
平方取中法:取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中的哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。取的位数由表长决定。
折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
随机数法
除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即hash(k)=k ,\bmod ,p, p\le m。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对p的选择很重要,一般取素数或m,若p选择不好,容易产生碰撞。
查找效率
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
散列函数是否均匀;
处理冲突的方法;
散列表的载荷因子(load factor)。
本文参考:
1.http://www.cnblogs.com/CBDoctor/p/4080217.html
2.http://www.cnblogs.com/Anker/archive/2013/01/27/2879150.html