数据结构算法之Hash(散列)

目录

一、简介

基本概念:

性质:

二、构造方法

2.1直接定址法

2.2数字分析法

3.平法取中法

4.折叠法

5.除留余数法

6.随机数法

三、处理冲突的方法

1.开发定址法

2. 再哈希法:

3.链地址法

4.建立一个公共溢出区

四、哈希表的查找及其分析

1.开发定址等方法(除链地址法外)处理冲突的哈希表的查找过程

2.调用1实现了开发定址哈希表的插入操作。


一、简介

     Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

基本概念:

  • 若结构中存在和关键字K相等的记录,则必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个事先建立的表为散列表

  • 对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称碰撞(冲突)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象” 作为记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。

  • 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

性质:

    所有散列函数都有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(可能出现哈希碰撞)。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。

典型的散列函数都有无限定义域,比如任意长度的字节字符串,和有限的值域,比如固定长度的比特串。在某些情况下,散列函数可以设计成具有相同大小的定义域和值域间的一一对应。一一对应的散列函数也称为排列。可逆性可以通过使用一系列的对于输入值的可逆“混合”运算而得到。

 注意:一般情况下,冲突只能尽可能地减少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。通常,关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为哈希表的地址值。

二、构造方法

    什么是好的哈希表?

若对一个关键字集合中的任一个关键字,经哈希函数映射到地址集合中任何一个地址的概率是相等的,则称此类哈希函数为均匀分布(Uniform)哈希函数。换句话说,就是使关键字经过哈希函数得到一个“随机的地址”,以便一组关键字的哈希地址均匀分布在整个地址区间中,从而减少冲突。

2.1直接定址法

取关键字或关键字的某个线性函数值为哈希地址。即

H(key)=key或H(key)=a*key+b(其中a和b为常数——这种函数叫做自身函数)。

由于直接定址所得地址集合和关键字集合的大小相同。因此,对于不同的关键字不会发生冲突。但实际中能使用这种哈希函数的情况很少。

2.2数字分析法

假设关键字是以r为基的数(如:以10为基的十进制数),并且哈希表中可能出现的关键字都事先知道的,则可去关键字的若干数位组成哈希地址。

例如有80个记录,其关键字为8位十进制数。假设哈希表的表长为100(10),则可取两位十进制数组成哈希地址。取哪两位?原则是使得得到的哈希地址尽量避免产生冲突,则需从分析这80个关键字着手。假设这两个关键字中的一部分如下列:                                                                                                             

对关键字全体的分析中我们发现:第1、2位都是“8 1”,第3位只可能取1、2、3或4,第8位只可能取2、5或7,因此这4位都不可取。由于中间的4位可看成是几乎随机的,因此可取其中任意两位,或取其中两位与另外两位的叠加求和后舍去进位作为哈希地址。

因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

3.平法取中法

取关键字平方后的中间几位为哈希地址。这是一种较常用的构造哈希函数的方法。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址是随机的。取的位数由表长决定。

例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如下图所示

关键字

内部编码

内部编码的平方值

H(k)关键字的哈希地址

KEYA

 11052501

122157778355001

778

KYAB

11250102

126564795010404

795

AKEY

01110525

001233265775625

265

BKEY

02110525

004454315775625

315

4.折叠法

将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。

5.除留余数法

取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 :

                            H(key) = key MOD p,p<=m。

不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。 

6.随机数法

选取一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key)=radom(key),其中random为随机函数。通常,当关键字长度不等时采用此法构造哈希函数较恰当。

实际工作中需视不同的情况采用不同的哈希函数。通常,考虑的因素有:

(1)计算哈希函数所需时间(包括硬件指令的因素);

(2)关键字的长度;

(3)哈希表的大小;

(4)关键字的分布情况;

(5)记录的查找频率。

三、处理冲突的方法

    假设哈希表的地址集为0~(n-1),冲突是指由关键字得到的哈希地址为j(0<=j<=n-1)的位置上已存有记录,则“处理冲突”就是该关键字的记录找到另一个“空”的哈希地址。在处理冲突的过程中可能得到一个地址序列Hi(i=1,2,...,k,),其中Hi属于0到n-1的闭区间。即在处理哈希地址的冲突时,若得到的另一个哈希地址H1仍然发生冲突,则再求下一个地址H2,若H2仍然再求得H3,。依次类推,直至Hk不发生冲突为止,则Hk为记录在表中的地址。

1.开发定址法

Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:

(1) di=1,2,3,…,m-1,称线性探测再散列;

(2) di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)称二次探测再散列;

(3) di=伪随机数序列,称伪随机探测再散列。

注意:用线性探测再散列处理冲突可以保证做到:只要哈希表未填满,总能找到一个不发生冲突的地址Hk,而二次探测再散列只有在哈希表长m为形如4j+3(j为整数)的素数时才可能,随机探测再散列,则取决于伪随机数列。

2. 再哈希法:

                                                                                          Hi=RHi(key)               i=1,2,…,k 

RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间

3.链地址法

    将所有关键字为同义词的记录存储在同一线性链表中。假设某哈希函数产生的哈希地址在区间0到m-1的闭区间上,则设立一个指针型向量

                                                      Chain ChainHash[m];

其每个分量的初始状态都是空指针。凡哈希地址为i的记录都插入到头指针为Chain Hash[i]的链表中。在链表中的插入位置可以在表头或表尾;也可以在中间,以保持同义词在同一线性链表中按关键字有序。                                                                                                                                                 

4.建立一个公共溢出区

    这也是处理冲突的一种方法。假设哈希函数打值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,每一个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突都填入溢出表。

四、哈希表的查找及其分析

    在哈希表上进行查找的过程和哈希造表打过程基本一致。给定K值,根据造表时设定的哈希函数求得哈希地址,若表中此位置上没有记录,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据造表时设定的冲突的方法找“下一地址”,直至哈希表中某个位置为“空”或表中所填记录的关键字等于给定值时为止。

1.开发定址等方法(除链地址法外)处理冲突的哈希表的查找过程

//***************开放定址哈希表的存储结构*********************

int hashsize[]={997,...};               //哈希表容量递增表,一个合适的素数序列

typedef struct{

    ElemType *elem;     //数据元素存储基址,动态分配数组

    int count;                 //当前数据元素个数

    int sizeindex;          //hashsize[sizeindex]为当前容量

}HashTable;

#define SUCCES 1

#define UNSUCCESS 0

#define DUPLICATE -1

Status SearchHash(HashTable H,KeyType k,int &p,int &c){

//在开放定址哈希表H中查找关键码为k的元素,若查找成功,以p指示待查数据

//元素在表中位置,并返回SUCCESS;否则,以p指示插入位置,并返回UNSUCCESS,

//c用以计冲突次数,其初值置零,供建表插入时参考

p=Hash(k);                     //求得哈希地址

while(H.elem[p].key!=NULLKEY&&      //该位置中填有记录

    !EQ(k,H.elem[p].key))                 //并且关键字不相等

collision(p,++c);                             //求得下一探查地址p

if EQ(K,H.elem[p].key)

    return SUCCESS;          //查找成功,p返回待查数据位置

else return UNSUCCESS;         //查找不成功(H.elem[p].key==NULLKEY),

                                            // p返回的是插入位置

}//SearchHash 

2.调用1实现了开发定址哈希表的插入操作。

Status InsertHash(HashTable &H,Elemtype e){

//查找不成功时插入数据元素e到开发定址哈希表H中,并返回Ok;若冲突次数过大,则重建哈希表

c=0;

if(searchHash(H,e.key,p,c))

    return DUPLICATE;           //表中已有与e相同关键字的元素

else if(c<hashsize[H.sizeindex]/2){                 //冲突次数c未达到上限,(c打阀值可调)

    H.elem[p]=e;++H.count; return OK;             //插入e

}

else {RecreateHashTable(H);return UNSUCCESS;}            //重建哈希表

}//InsertHash

实例:                                                                                                                             

注意:线性探测再散列在处理冲突的过程中容易产生记录的二次聚集,如既使得哈希地址不相同的记录又产生新的冲突;而链地址法处理冲突不会发生类似情况,因为哈希地址不同的记录在不同的链表中。

在一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子。

哈希表的装填因子定义为

α=表中填入的记录数/哈希表的长度

α标志哈希表的装满程度。直观地看,α越小,发生冲突的可能性就越小;反之,α越大,表中已填入的记录越多,再填记录时,发生冲突的可能性就越大,则查找时,给定值需与之进行比较的关键字个个数也多。

                                                                         

以上内容主要摘自《数据结构》严蔚敏版一书,详细实现例子可参考其他资料或blog

猜你喜欢

转载自blog.csdn.net/idealhunting/article/details/82795003
今日推荐