数据结构与算法18—哈希表(散列表)

哈希表的概念数组

哈希表(Hash Table)是一种特殊的数据结构,它最大的特色就是能够快速实现查找、插入和删除。数据结构

咱们知道,数组的最大特色就是:寻址容易,插入和删除困难;而链表正好相反,寻址困难,而插入和删除操做容易。那么若是可以结合二者的优势,作出一种寻址、插入和删除操做一样快速容易的数据结构,那该有多好。这就是哈希表建立的基本思想,而实际上哈希表也实现了这样的一个“夙愿”,哈希表就是这样一个集查找、插入和删除操做于一身的数据结构。函数

 

哈希表(Hash Table):也叫散列表,是根据关键码值(key-value)而直接进行访问的数据结构,也就是咱们经常使用到的map。加密

哈希函数:也称为是散列函数,是Hash表的映射函数,它能够把任意长度的输入变换成固定长度的输出,该输出就是哈希值。哈希函数能使对一个数据序列的访问过程变得更加迅速有效,经过哈希函数,数据元素可以被很快的进行定位。spa

 

哈希表和哈希函数的标准定义:若关键字为k,则其值存放在h(k)的存储位置上。由此,不需比较即可直接取得所查记录。称这个对应关系f为哈希函数,按这个思想创建的表为哈希表。设计

 

设计出一个简单、均匀、存储利用率高的散列函数是散列技术中最关键的问题。
可是,通常散列函数都面临着冲突的问题。两个不一样的关键字,因为散列函数值相同,于是被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。发生冲突的两个关键字称为该散列函数的同义词(Synonym)。
3d

 

设m和n分别表示表长和表中填入的结点数,则将α=n/m定义为散列表的装填因子(Load Factor)。α越大,表越满,冲突的机会也越大。一般取α≤1。指针

 

哈希表的实现方法

哈希表的实现就是映射函数构造,看某个元素具体属于哪个类别。如何构造咱们要考虑两个问题:code

  • n个数据原仅占用n个地址,虽然散列查找是以空间换时间,但仍但愿散列的地址空间尽可能小。
  • 不管用什么方法存储,目的都是尽可能均匀地存放元素,以免冲突。blog

因此,我哈希表的映射函数构造方法也有不少,常见的有:直接定址法、 除留余数法、 乘余取整法、 数字分析法、 平方取中法、 折叠法、 随机数法等

 

一、直接定位法

Hash(key) = a·key + b (a、b为常数)

优势:以关键码key的某个线性函数值为哈希地址,不会产生冲突.

缺点:要占用连续地址空间,空间效率低。

例:关键码集合为{100,300,500,700,800,900}, 选取哈希函数为Hash(key)=key/100, 则存储结构(哈希表)以下:

 

二、除留余数法

Hash(key) = key mod p (p是一个整数)

特色:以关键码除以p的余数做为哈希地址。

关键:如何选取合适的p?

技巧:若设计的哈希表长为m,则通常取p≤m且为质数 (也能够是不包含小于20质因子的合数)。

 

三、乘余取整法

Hash(key) = [B*( A*key mod 1 ) ]下取整  (A、B均为常数,且0<A<1,B为整数)

特色:以关键码key乘以A,取其小数部分,而后再放大B倍并取整,做为哈希地址。

例:欲以学号最后两位做为地址,则哈希函数应为: H(k)=100*(0.01*k % 1 ) 其实也能够用法2实现: H(k)=k % 100

 

四、数字分析法

特色:某关键字的某几位组合成哈希地址。所选的位应当是:各类符号在该位上出现的频率大体相同。

例:有一组(例如80个)关键码,其样式以下:

 

五、平方取中法

特色:对关键码平方后,按哈希表大小,取中间的若干位做为哈希地址。

理由:由于中间几位与数据的每一位都相关。

:2589的平方值为6702921,能够取中间的029为地址。

 

六、折叠法

特色:将关键码自左到右分红位数相等的几部分(最后一部分位数能够短些),而后将这几部分叠加求和,并按哈希表表长,取后几位做为哈希地址。

适用于:每一位上各符号出现几率大体相同的状况。

法1:移位法 ── 将各部分的最后一位对齐相加。

法2:间界叠加法──从一端向另外一端沿分割界来回折叠后,最后一位对齐相加。

例:元素42751896, 用法1: 427+518+96=1041      用法2: 427 518 96—> 724+518+69 =1311

 

哈希表定址与解决冲突

Hash表解决冲突的方法主要有如下几种:

开放定址法(开地址法)、 链地址法(拉链法)、 再哈希法(双哈希函数法)、 创建一个公共溢出区,而最经常使用的就是开发定址法链地址法

 

一、开放定址法

若是两个数据元素的哈希值相同,则在哈希表中为后插入的数据元素另外选择一个表项。当程序查找哈希表时,若是没有在第一个对应的哈希表项中找到符合查找要求的数据元素,程序就会继续日后查找,直到找到一个符合查找要求的数据元素,或者遇到一个空的表项。线性探测带来的最大问题就是冲突的堆积,你把别人预约的坑占了,别人也就要像你同样去找坑。改进的办法有二次方探测法和随机数探测法。开放地址法包括线性探测二次探测以及双重散列等方法。

设计思路:有冲突时就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将数据元素存入。

含义:一旦冲突,就找附近(下一个)空地址存入。

具体实现:

1) 线性探测法

Hi=(Hash(key)+di) mod m  ( 1≤i < m )    其中: Hash(key)为哈希函数  m为哈希表长度  di 为增量序列 1,2,…m-1,且di=i

例:

关键码集为 {47,7,29,11,16,92,22,8,3},

设:哈希表表长为m=11; 哈希函数为Hash(key)=key mod 11; 拟用线性探测法处理冲突。建哈希表以下:

 

解释:

 ① 4七、7(以及十一、1六、92)均是由哈希函数获得的没有冲突的哈希地址;

② Hash(29)=7,哈希地址有冲突,需寻找下一个空的哈希地址:由H1=(Hash(29)+1) mod 11=8,哈希地址8为空,所以将29存入。

③ 另外,2二、八、3一样在哈希地址上有冲突,也是由H1找到空的哈希地址的。其中3 还连续移动了两次(二次汇集)

int FindHash(SeqList* pL, KeyType K) 
{
    int c=0;  int p=Hash(K); /*求得哈希地址*/
    while(pL->data[p].key!=NULL_KEY && K!=pL->data[p].key && ++c<MAXNUM)    p=Hash(K+c);
    if(K==pL->data[p].key) {
        printf("\n成功找到 %d", K);
        return p; /*查找成功,p返回待查数据元素下标*/
    }
    else if(pL->data[p].key==NULL_KEY) {
       printf("\n没法找到 %d , 在位置 %d 插入。", K,p);
       pL->data[p].key = K;   pL->n++;
       return p;
    } else {
       printf("\n没法找到 %d , 表已满。", K);
       return -1;
    }
}

讨论:

线性探测法的优势只要哈希表未被填满,保证能找到一个空地址单元存放有冲突的元素;

线性探测法的缺点:可能使第i个哈希地址的同义词存入第i+1个哈希地址,这样本应存入第i+1个哈希地址的元素变成了第i+2个哈希地址的同义词,……,所以,可能出现不少元素在相邻的哈希地址上“堆积”起来,大大下降了查找效率。

解决方案:可采用二次探测法或伪随机探测法,以改善“堆积”问题。

 

2) 二次探测法

仍举上例,改用二次探测法处理冲突,建表以下:

Hi=(Hash(key)±di) mod m   其中:Hash(key)为哈希函数 m为哈希表长度,m要求是某个4k+3的质数; di为增量序列 12,-12,22,-22,…,q2

注:只有3这个关键码的冲突处理与上例不一样, Hash(3)=3,哈希地址上冲突,由 H1=(Hash(3)+12) mod 11=4,仍然冲突; H2=(Hash(3)-12) mod 11=2,找到空的哈希地址,存入。

 

 2、链地址法(拉链法)

基本思想:将具备相同哈希地址的记录链成一个单链表,m个哈希地址就设m个单链表,而后用一个数组将m个单链表的表头指针存储起来,造成一个动态的结构。

注:有冲突的元素能够插在表尾,也能够插在表头

:设{ 47, 7, 29, 11, 16, 92, 22, 8, 3, 50, 37, 89 }的哈希函数为: Hash(key)=key mod 11, 用拉链法处理冲突,则建表以下图所示。

 

三、再哈希法(双哈希函数法)

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

RHi均是不一样的哈希函数,当产生冲突时就计算另外一个哈希函数,直到冲突再也不发生。

优势:不易产生汇集;

缺点:增长了计算时间。

 

4. 创建一个公共溢出区

思路:除设立哈希基本表外,另设立一个溢出向量表。 全部关键字和基本表中关键字为同义词的记录,无论它们由哈希函数获得的地址是什么,一旦发生冲突,都填入溢出表。

这个方法其实就更加好理解,你不是冲突吗? 好吧,凡是冲突的都跟我走,我给大家这些冲突找个地儿待着。这就如同孤儿院收留全部无家可归的孩子 样,咱们为全部冲突的关键字创建了一个公共的溢出区来存放.

 

哈希表的查找及分析

哈希查找过程

哈希表的主要目的是用于快速查找,且插入和删除操做都要用到查找。因为散列表的特殊组织形式,其查找有特殊的方法。 设散列为HT[0…m-1],散列函数为H(key),解决冲突的方法为R(x, i) ,则在散列表上查找定值为K的记录的过程如图所示。

 

查找效率分析

明确:散列函数没有“万能”通式,要根据元素集合的特性而分别构造。

讨论:哈希查找的速度是否为真正的O(1)?

不是。因为冲突的产生,使得哈希表的查找过程仍然要进行比较,仍然要以平均查找长度ASL来衡量。 通常地,ASL依赖于哈希表的装填因子,它标志着哈希表的装满程度。

 

讨论:

1) 散列存储的查找效率究竟是多少?

答:ASL与装填因子α有关!既不是严格的O(1),也不是O(n)

2)“冲突”是否是特别讨厌?

答:不必定!正由于有冲突,使得文件加密后没法破译(不可逆,是单向散列函数,可用于数字签名)。 利用了哈希表性质:源文件稍稍改动,会致使哈希表变更很大。

 

 练习: 

给定关键字序列11,78,10,1,3,2,4,21,试分别用顺序查找、二分查找、二叉排序树查找、散列查找(用线性探查法和拉链法)来实现查找,试画出它们的对应存储形式(顺序查找的顺序表,二分查找的断定树,二叉排序树查找的二叉排序树及两种散列查找的散列表),并求出每一种查找的成功平均查找长度。散列函数H(k)=k%11

 

1)  顺序查找的成功平均查找长度为: ASL=(1+2+3+4+5+6+7+8)/8=4.5

 

2)  二分查找的断定树(中序序列为从小到大排列的有序序列)如图所示

从上图能够获得二分查找的成功平均查找长度为: ASL=(1+2*2+3*4+4)/8=2.625

 

3)  二叉排序树(关键字顺序已肯定,该二叉排序树应惟一)如图 所示

从图能够获得二叉排序树查找的成功平均查找长度为: ASL=(1+2*2+3*2+4+5*2)=3.125

 

4)  线性探查法解决冲突的散列表如图所示

从图能够获得线性探查法的成功平均查找长度为: ASL=(1+1+2+1+3+2+1+8)/8=2.375

 

5)  拉链法解决冲突的散列表 如图所示

从图能够获得拉链法的成功平均查找长度为: ASL=(1*6+2*2)/8=1.25

相关文章
相关标签/搜索