散列技术

1. 前言

    散列方法不同于顺序查找、二分查找、二叉排序树及B-树上的查找。它不以关键字的比较为基本操作,采用直接寻址技术。在理想情况下,无须任何比较就可以找到待查关键字,查找的期望时间为O(1)。

2. 散列表的定义

    设所有可能出现的关键字集合记为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)

3. 散列表的冲突

(1)冲突
     两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。发生冲突的两个关键字称为该散列函数的同义词(Synonym)。
   【例】上图中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的结点的存储地址相同。

(2)安全避免冲突的条件
     最理想的解决冲突的方法是安全避免冲突。要做到这一点必须满足两个条件:
①其一是|U|≤m
②其二是选择合适的散列函数。
     这只适用于|U|较小,且关键字均事先已知的情况,此时经过精心设计散列函数h有可能完全避免冲突。

(3)冲突不可能完全避免
     通常情况下,h是一个压缩映像。虽然|K|≤m,但|U|>m,故无论怎样设计h,也不可能完全避免冲突。因此,只能在设计h时尽可能使冲突最少。同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中。

(4)影响冲突的因素
     冲突的频繁程度除了与h相关外,还与表的填满程度相关。
     设m和n分别表示表长和表中填人的结点数,则将α=n/m定义为散列表的装填因子(Load Factor)。α越大,表越满,冲突的机会也越大。通常取α≤1。

4. 散列函数的构造方法

    散列函数的选择有两条标准:简单和均匀。
    简单指散列函数的计算简单快速;
    均匀指对于关键字集合中的任一关键字,散列函数能以等概率将其映射到表空间的任何一个位置上。也就是说,散列函数能将子集K随机均匀地分布在表的地址集{0,1,…,m-1}上,以使冲突最小化。

(1)平方取中法
     具体方法:先通过求关键字的平方值扩大相近数的差别,然后根据表长度取中间的几位数作为散列函数值。又因为一个乘积的中间几位数和乘数的每一位都相关,所以由此产生的散列地址较为均匀。
   【例】将一组关键字(0100,0110,1010,1001,0111)平方后得
    (0010000,0012100,1020100,1002001,0012321)
   若取表长为1000,则可取中间的三位数作为散列地址集:
    (100,121,201,020,123)。
相应的散列函数用C实现很简单:
int Hash(int key){ //假设key是4位整数
  key*=key; key/=100; //先求平方值,后去掉末尾的两位数
  return key%1000; //取中间三位数作为散列地址返回
 }

(2)除余法

     该方法是最为简单常用的一种方法。它是以表长m来除关键字,取其余数作为散列地址,即 h(key)=key%m
     该方法的关键是选取m。选取的m应使得散列函数值尽可能与关键字的各位相关。m最好为素数。
   【例】若选m是关键字的基数的幂次,则就等于是选择关键字的最后若干位数字作为地址,而与高位无关。于是高位不同而低位相同的关键字均互为同义词。
   【例】若关键字是十进制整数,其基为10,则当m=100时,159,259,359,…,等均互为同义词。

(3)相乘取整法
     该方法包括两个步骤:首先用关键字key乘上某个常数A(0<A<1),并抽取出key.A的小数部分;然后用m乘以该小数后取整。即:
           
     该方法最大的优点是选取m不再像除余法那样关键。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取
           
     该函数的C代码为:
int Hash(int key){
  double d=key *A; //不妨设A和m已有定义
  return (int)(m*(d-(int)d));//(int)表示强制转换后面的表达式为整数
 }

(4)随机数法
     选择一个随机函数,取关键字的随机函数值为它的散列地址,即
         h(key)=random(key)
   其中random为伪随机函数,但要保证函数值是在0到m-1之间。

5. 处理冲突的方法

    通常有两类方法处理冲突:开放定址(Open Addressing)法和拉链(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是将互为同义词的结点链成一个单链表,而将此链表的头指针放在散列表T[0..m-1]中。

<1> 开放地址法

(1)开放地址法的一般形式
     开放定址法的一般形式为: hi=(h(key)+di)%m 1≤i≤m-1 
其中:
     ①h(key)为散列函数,di为增量序列,m为表长。
     ②h(key)是初始的探查位置,后续的探查位置依次是hl,h2,…,hm-1,即h(key),hl,h2,…,hm-1形成了一个探查序列。
     ③若令开放地址一般形式的i从0开始,并令d0=0,则h0=h(key),则有:
          hi=(h(key)+di)%m 0≤i≤m-1 
       探查序列可简记为hi(0≤i≤m-1)。

(2)开放地址法堆装填因子的要求
     开放定址法要求散列表的装填因子α≤l,实用中取α为0.5到0.9之间的某个值为宜。

(3)形成探测序列的方法
     按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。
①线性探查法(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

聚集或堆积现象
     用线性探查法解决冲突时,当表中i,i+1,…,i+k的位置上已有结点时,一个散列地址为i,i+1,…,i+k+1的结点都将插入在位置i+k+1上。把这种散列地址不同的结点争夺同一个后继散列地址的现象称为聚集或堆积(Clustering)。这将造成不是同义词的结点也处在同一个探查序列之中,从而增加了探查序列的长度,即增加了查找时间。若散列函数不好或装填因子过大,都会使堆积现象加剧。
  【例】上例中,h(15)=2,h(68)=3,即15和68不是同义词。但由于处理15和同义词41的冲突时,15抢先占用了T[3],这就使得插入68时,这两个本来不应该发生冲突的非同义词之间也会发生冲突。
     为了减少堆积的发生,不能像线性探查法那样探查一个顺序的地址序列(相当于顺序查找),而应使探查序列跳跃式地散列在整个散列表中。

 ②二次探查法(Quadratic Probing)
    二次探查法的探查序列是:
         hi=(h(key)+i*i)%m 0≤i≤m-1 //即di=i2
   即探查序列为d=h(key),d+12,d+22,…,等。
     该方法的缺陷是不易探查到整个散列空间。

③双重散列法(Double Hashing)
     该方法是开放定址法中最好的方法之一,它的探查序列是:
       hi=(h(key)+i*h1(key))%m 0≤i≤m-1 //即di=i*h1(key)
     即探查序列为:
       d=h(key),(d+h1(key))%m,(d+2h1(key))%m,…,等。
   该方法使用了两个散列函数h(key)和h1(key),故也称为双散列函数探查法。
注意:
     定义h1(key)的方法较多,但无论采用什么方法定义,都必须使h1(key)的值和m互素,才能使发生冲突的同义词地址均匀地分布在整个表中,否则可能造成同义词地址的循环计算。
  【例】 若m为素数,则h1(key)取1到m-1之间的任何数均与m互素,因此,我们可以简单地将它定义为:
               h1(key)=key%(m-2)+1
  【例】对例9.1,我们可取h(key)=key%13,而h1(key)=key%11+1。
  【例】若m是2的方幂,则h1(key)可取1到m-1之间的任何奇数。

 

<2> 拉链法

(1)拉链法解决冲突的方法
     拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

【例9.2】已知一组关键字和选定的散列函数和例9.1相同,用拉链法解决冲突构造这组关键字的散列表。
  解答:不妨和例9.1类似,取表长为13,故散列函数为h(key)=key%13,散列表为T[0..12]。
注意:
     当把h(key)=i的关键字插入第i个单链表时,既可插入在链表的头上,也可以插在链表的尾上。这是因为必须确定key不在第i个链表时,才能将它插入表中,所以也就知道链尾结点的地址。若采用将新关键字插入链尾的方式,依次把给定的这组关键字插入表中,则所得到的散列表如下图所示。

                                          

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值