有关散列表

来源:互联网 发布:linux执行命令的过程 编辑:程序博客网 时间:2024/06/08 17:10

散列表的概念
注意:
  ①由同一个散列函数、不同的解决冲突方法构造的散列表,其平均查找长度是不相同的。
 ②散列表的平均查找长度不是结点个数n的函数,而是装填因子α的函数。因此在设计散列表时可选择α以控制散列表的平均查找长度。
通过链接法解决冲突:成功查找的期望查找长度O(1+a), 不成功查找的平均查找长度也为O(1+a)。
开放寻址解决冲突:引入探查序列,对于a<1的开放寻址,成功查找的平均查找长度1/a(1+ln(1/(1-a)); 不成功的查找长度为1/(1-a)

  ③ α的取值  α越小,产生冲突的机会就小,但α过小,空间的浪费就过多。只要α选择合适,散列表上的平均查找长度就是一个常数,即散列表上查找的平均时间为O(1)。  ④ 散列法与其他查找方法的区别

除散列法外,其他查找方法有共同特征为:均是建立在比较关键字的基础上。
其中顺序查找平均时间为O(n);
其余的查找均是对有序集合的查找,每次关键字的比较有”=”、”<”和”>”三种可能,且每次比较后均能缩小下次的查找范围,故查找速度更快,其平均时间为O(lgn)。
而散列法是根据关键字直接求出地址的查找方法,其查找的期望时间为O(1)。

1、散列表
 设所有可能出现的关键字集合记为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。
散列函数的构造方法

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

2、常用散列函数
 为简单起见,假定关键字是定义在自然数集合上。

(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

define NIL -1 //空结点标记依赖于关键字类型,本节假定关键字均为非负整数

define M 997 //表长度依赖于应用,但一般应根据。确定m为一素数

typedef struct{ //散列表结点类型
KeyType key;
InfoType otherinfo; //此类依赖于应用
}NodeType;
typedef NodeType HashTable[m]; //散列表类型

2、基于开放地址法的查找算法
 散列表的查找过程和建表过程相似。假设给定的值为K,根据建表时设定的散列函数h,计算出散列地址h(K),若表中该地址单元为空,则查找失败;否则将该地址中的结点与给定值K比较。若相等则查找成功,否则按建表时设定的处理冲突的方法找下一个地址。如此反复下去,直到某个地址单元为空(查找失败)或者关键字比较相等(查找成功)为止。

(1)开放地址法一般形式的函数表示
int Hash(KeyType k,int i)
{ //求在散列表T[0..m-1]中第i次探查的散列地址hi,0≤i≤m-1
//下面的h是散列函数。Increment是求增量序列的函数,它依赖于解决冲突的方法
return(h(K)+Increment(i))%m; //Increment(i)相当于是di
}
若散列函数用除余法构造,并假设使用线性探查的开放定址法处理冲突,则上述函数中的h(K)和Increment(i)可定义为:
int h(KeyType K){ //用除余法求K的散列地址
return K%m;
}

int Increment(int i){//用线性探查法求第i个增量di
return i; //若用二次探查法,则返回i*i
}
(2)通用的开放定址法的散列表查找算法:
int HashSearch(HashTable T,KeyType K,int *pos)
{ //在散列表T[0..m-1]中查找K,成功时返回1。失败有两种情况:找到一个开放地址
//时返回0,表满未找到时返回-1。 *pos记录找到K或找到空结点时表中的位置
int i=0; //记录探查次数
do{
*pos=Hash(K,i); //求探查地址hi
if(T[*pos].key==K) return l; //查找成功返回
if(T[*pos].key==NIL) return 0;//查找到空结点返回
}while(++i < m) abp=”862” 最多做m次探查
return -1; //表满且未找到时,查找失败
} //HashSearch

注意:
 上述算法适用于任何开放定址法,只要给出函数Hash中的散列函数h(K)和增量函数Increment(i)即可。但要提高查找效率时,可将确定的散列函数和求增量的方法直接写入算法HashSearch中,相应的算法【参见习题】。

3、基于开放地址法的插入及建表
 建表时首先要将表中各结点的关键字清空,使其地址为开放的;然后调用插入算法将给定的关键字序列依次插入表中。
 插入算法首先调用查找算法,若在表中找到待插入的关键字或表已满,则插入失败;若在表中找到一个开放地址,则将待插入的结点插入其中,即插入成功。
void Hashlnsert(HashTable T,NodeTypene w)
{ //将新结点new插入散列表T[0..m-1]中
int pos,sign;
sign=HashSearch(T,new.key,&pos); //在表T中查找new的插入位置
if(!sign) //找到一个开放的地址pos
T[pos]=new; //插入新结点new,插入成功
else //插人失败
if(sign>0)
printf(“duplicate key!”); //重复的关键字
else //sign<0
Error(“hashtableoverflow!”); //表满错误,终止程序执行
} //Hashlnsert

void CreateHashTable(HashTable T,NodeType A[],int n)
{ //根据A[0..n-1]中结点建立散列表T[0..m-1]
int i
if(n>m) //用开放定址法处理冲突时,装填因子α须不大于1
Error(“Load factor>1”);
for( i = 0; i < m;i++)
T[i].key=NIL; //将各关键字清空,使地址i为开放地址
for(i=0;i < n;i++) abp=”894” 依次将a[0..n-1]插入到散列表t[0..m-1]中
Hashlnsert(T,A[i]);
} //CreateHashTable

4、删除
 基于开放定址法的散列表不宜执行散列表的删除操作。若必须在散列表中删除结点,则不能将被删结点的关键字置为NIL,而应该将其置为特定的标记DELETED。
 因此须对查找操作做相应的修改,使之探查到此标记时继续探查下去。同时也要修改插人操作
 将新结点插入其中。这样做无疑增加了时间开销,并且查找时间不再依赖于装填因子。
 因此,当必须对散列表做删除结点的操作时,一般是用拉链法来解决冲突。
 5、性能分析
 插入和删除的时间均取决于查找,故下面只分析查找操作的时间性能。
 虽然散列表在关键字和存储位置之间建立了对应关系,理想情况是无须关键字的比较就可找到待查关键字。但是由于冲突的存在,散列表的查找过程仍是一个和关键字比较的过程,不过散列表的平均查找长度比顺序查找、二分查找等完全依赖于关键字比较的查找要小得多。

(1)查找成功的ASL
 散列表上的查找优于顺序查找和二分查找。
  【例】在例9.1和例9.2的散列表中,在结点的查找概率相等的假设下,线性探查法和拉链法查找成功的平均查找长度分别为:
ASL=(1×6+2×2+3×l+9×1)/10=2.2 //线性探查法
ASL=(1×7+2×2+3×1)/10=1.4 //拉链法
  而当n=10时,顺序查找和二分查找的平均查找长度(成功时)分别为:
ASL=(10+1)/2=5.5 //顺序查找
ASL=(1×l+2×2+3×4+4×3)/10=2.9 //二分查找,可由判定树求出该值

(2) 查找不成功的ASL
 对于不成功的查找,顺序查找和二分查找所需进行的关键字比较次数仅取决于表长,而散列查找所需进行的关键字比较次数和待查结点有关。因此,在等概率情况下,也可将散列表在查找不成功时的平均查找长度,定义为查找不成功时对关键字需要执行的平均比较次数。
  【例】例9.1和例9.2的散列表中,在等概率情况下,查找不成功时的线性探查法和拉链法的平均查找长度分别为:
ASLunsucc=(9+8+7+6+5+4+3+2+1+1+2+1+10)/13=59/13≈4.54
ASLunsucc=(1+0+2+1+0+1+1+0+0+0+1+0+3)/13≈10/13≈0.77

原创粉丝点击