转载:哈希函数 形象说明

哈希表与哈希函数

 

     hash表,有时候也被称为散列表。个人认为,hash表是介于链表和二叉树之间的一种中间结构。链表使用十分方便,但是数据查找十分麻烦;二叉树中的数据严格有序,但是这是以多一个指针作为代价的结果。hash表既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

    打个比方来说,所有的数据就好像许许多多的书本。如果这些书本是一本一本堆起来的,就好像链表或者线性表一样,整个数据会显得非常的无序和凌乱,在你找到自己需要的书之前,你要经历许多的查询过程;而如果你对所有的书本进行编号,并且把这些书本按次序进行排列的话,那么如果你要寻找的书本编号是n,那么经过二分查找,你很快就会找到自己需要的书本;但是如果你每一个种类的书本都不是很多,那么你就可以对这些书本进行归类,哪些是文学类,哪些是艺术类,哪些是工科的,哪些是理科的,你只要对这些书本进行简单的归类,那么寻找一本书也会变得非常简单,比如说如果你要找的书是计算机方面的书,那么你就会到工科一类当中去寻找,这样查找起来也会显得麻烦。

 

 

       哈希(Hash)算法就是单向散列算法,它把某个较大的集合P映射到另一个较小的集合Q中,假如这个算法叫H,那么就有Q = H(P)。对于P中任何一个值p都有唯一确定的q与之对应,但是一个q可以对应多个p。作为一个有用的Hash算法,H还应该满足:H(p)速度比较快;给出一个q,很难算出一个p满足q = H(p);给出一个p1,很难算出一个不等于p1的p2使得 H(p1)=H(p2)。

       数学原理听起来很抽象,在网上找到一个很生动的描述。我们有很多的小猪,每个的体重都不一样,假设体重分布比较平均(我们考虑到公斤级别),我们按照体重来分,划分成100个小猪圈。 然后把每个小猪,按照体重赶进各自的猪圈里,记录档案。

       好了,如果我们要精确找到某个小猪怎么办呢?我们需要每个猪圈,每个小猪的比对吗? 当然不需要了。 我们先看看要找的这个小猪的体重,然后就找到了对应的猪圈了。 在这个猪圈里的小猪的数量就相对很少了。 我们在这个猪圈里就可以相对快的找到我们要找到的那个小猪了。

        对应回hash算法:就是按照hashcode分配不同的猪圈,将hashcode相同的猪放到一个猪圈里。 查找的时候,先找到hashcode对应的猪圈,然后在逐个比较里面的小猪。

       关键就是建造多少个猪圈比较合适。如果每个小猪的体重全部不同(考虑到毫克级别),每个都建一个猪圈,那么我们可以最快速度的找到这头猪。缺点就是,建造那么多猪圈的费用有点太高了。 如果我们按照10公斤级别进行划分,那么建造的猪圈只有几个吧,那么每个圈里的小猪就很多了。我们虽然可以很快的找到猪圈,但从这个猪圈里逐个确定那头小猪也是很累的。 所以,好的hashcode,可以根据实际情况,根据具体的需求,在时间成本(更多的猪圈,更快的速度)和空间本(更少的猪圈,更低的空间需求)之间平衡。

         所以一个简单的定义:哈希算法其本质上就是将一个数据映射成另一个数据,通常情况下原数据的长度比hash后的数据容量大。这种映射的关系我们叫做哈希函数或者散列函数。散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。常见的构造散列函数的方法有:

  • 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a×key + b,其中a和b为常数(这种散列函数叫做自身函数)
  • 数字分析法
  • 折叠法
  • 随机数法
  • 求模取余法

最经典的莫过于求模取余法。我们知道,任给一个整数A,将自然数1,2,3,4,…依次除以A,所得的余数总是循环出现,呈周期性变化, 所以,我们可以取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key % p, p<=m。

假设我们有一个很大集合A中有{496,387,184,21,96,31,.....}等等元素,回忆我们上面提到的小猪问题,我们可以将大的集合A(小猪)映射到一个小的集合B(猪圈)(假设B只有16个元素,请参考下图)。我们对元素A的每一个元素采用求模算法,得到: 496 % 16 = 0, 所以我们把496填入集合B的0号位置,387 % 16 = 3,那么387被填入集合B的3号位置。

 

当我们查询140是否在集合A中时,我们可以对140进行同样的求模算法,140 % 16=12 ,如果集合B的12号位置为空,就可以推断140不在集合A之中。但是,如果12号位置不为空,是否可以确定140在集合A之中呢?答案是否定的,主要是由于求模算法会对数组长度进行取余,因此其结果由于数组长度的限制必然会出现重复,比方说{108,12,140,28},这些元素用上面的算法得到的余数都是12,所以就会有“冲突”这一问题。解决冲突的方法有很多种,最直观的莫过于”拉链法“,即12号位置填入的不是元素本身,而是一个链表,所有余数相同的元素,都写入该链表。显然链表中的元素要远比集合A中的元素少了很多,这时就可以对链表做遍历比较了。

  从上面的例子,我们知道对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词,即所谓的“冲突”或“碰撞”。发生“冲突”的概率可以用装填因子来表示,装填因子Load factor a=哈希表的实际元素数目(n)/ 哈希表的容量(m) a越大,哈希表冲突的概率越大,但是a越接近0,那么哈希表的空间就越浪费。

一般情况下建议Load factor的值为0-0.7,Java实现的HashMap默认的Load factor的值为0.75,当装载因子大于这个值的时候,HashMap会对数组进行扩张至原来两倍大。

  哈希查找因使用哈希 (Hash) 函数而得名,哈希函数又叫散列函数,它是一种能把关键字映射成记录存贮地址的函数。 
一.哈希表
①它是一种能把关键字映射成记录存贮地址的函数。
②假定数组 HT[0 ~ m-1] 为存贮记录的地址空间, m 为表长,哈希函数 H 以记录的关键字 K为自变量,计算出对应的函数值 H(K) ,并以它作为关键字 K 所标识的记录在表 HT 中的 ( 相对) 地址或索引号,这样产生的记录表 HT 叫做对应于哈希函数 H 的哈希表
③简言之,在哈希表中,关键字为 K 的记录,存贮在 HT[H(K)] 位置。
④哈希函数值 H(K) 称为 K 的哈希地址或散列地址。

二.构造哈希表 
  构造哈希函数的方法很多,这里只介绍一些常用的,计算简便的方法。
1.平方取中法
  算出关键字值的平方,再取其中若干位作为哈希函数值 ( 散列地址 ) 。
【例】假定表中各关键字是由字母组成的,用二位数字的整数 01 ~ 26 表示对应的 26 个英文字母在计算机中的内部编码,则使用平方取中法计算 KEYA , KEYB , AKEY , BKEY 的散列地址可得:
关键字 K     K 的内部编码            K 2           H(K)
 KEYA         11052501       122157778355001      778
 KEYB         11052502       122157800460004      800
 AKEY         01110525       001233265775625      265
 BKEY         02110525       004454315775625      315
平方之后,取左起第 7 ~ 9 位作为散列地址。

2.除留余数法
    这种方法是用模运算 (%) 得到的。设给出的关键字值为 K ,存储区单元数为 m ,则用一个小于 m 的质数 P 去除 K ,得到的余数为 R ,即: R = K % P 。如果 R 落在存储区地址范围内,则 R 就取为哈希函数值 ( 散列地址 ) ;否则,再用一个线性数求出哈希函数值。
【例】有一组关键字从 000001 到 859999 ,指定的存储区地址为 1000000 ~ 1005999 ,即 m= 6000 ,可选 P = 599 ,若要转换关键字 K = 172148 ,则有:
                R = 172148 % 599 = 4176
因 R 不在指定的地址范围内,所以,取哈希函数为:
                  H(K) = 1000000 + R
故有:
                H(K) = H(172148) = 1004176
这样就把关键字 K 直接转换成存储地址了。

三.可能产生的冲突:

(1)冲突
     不同的关键字值,具有相同的哈希地址,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。
   【例】上图中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的结点的存储地址相同。

(2)安全避免冲突的条件
    如何避免冲突发生,则取决于哈希函数的构造。 
    使散列地址均匀地分布在哈希表的整个地址区间内,这样可以避免或减少发生冲突。
    哈希函数的构造,与关键字的长度、哈希表的大小、关键字的实际取值状况等许多因素有关,而且有的因素事前不能确定。所以,避免冲突这并非是件容易做到的事。
(3)冲突不可能完全避免
     由于关键字的值域往往比哈希表的个数大的多,所以哈希函数是一种压缩映射,碰撞是难免的。
   【例】存贮 100 个学生记录,尽管安排 120 个地址空间,但由于学生名 ( 假设不超过 10 个英文字母 ) 的理论个数超过 2610 ,要找到一个哈希函数把 100 个任意的学生名映射成 [0 ,119] 内的不同整数,实际上是不可能的。
   注意:问题在于一旦发生了冲突应如何处理。

四.解决冲突的主要方法 
   虽然我们不希望发生冲突,但实际上发生冲突的可能性仍是存在的。当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。冲突就难免会发 生。另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出。因此,处理冲突和溢出是 哈希技术中的两个重要问题。
1、开放定址法
     用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的地址则表明表中无待查的关键字,即查找失败。
  注意:
 ①用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。
 ②空单元的表示与具体的应用相关。
     按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。
(1)线性探查法(Linear Probing)
该方法的基本思想是:
     将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
        d,d+l,d+2,…,m-1,0,1,…,d-1
     即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
     (1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
     (2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
     (3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
利用开放地址法的一般形式,线性探查法的探查序列为:
        hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
  ① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
  ② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
  ③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。

(2)线性补偿探测法 
线性补偿探测法的基本思想是:
  将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j +Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m= 1321 ,选用 Q = 25 。

(3)随机探测 
随机探测的基本思想是:
  将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2、拉链法
(1)拉链法解决冲突的方法
     拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

(2)拉链法的优点
与开放定址法相比,拉链法有如下几个优点:
  ①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  ②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  ③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  ④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

(3)拉链法的缺点
     拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章