闭散列,开散列其实就是处理冲突的方式;
哈希冲突的处理:
方法一:
闭散列(即开放地址法):当发生哈希冲突时,如果该哈希表还没有被填满,那么就把该元素放到哈希表的下一个空闲的位置。
优缺点下面介绍;
开散列法(哈希桶):又名链地址法,先用哈希函数计算每个数据的散列地址,把具有相同地址的元素归于同一个集合之中,把该集合处理为一个链表,链表的头节点存储于哈希表之中。 优缺点下面介绍;
哈希之开散列和闭散列:
目的:实现一种结构,不经过任何比较,一次直接得到想要的元素。通过某种函数使元素的存储位置与它的关键码之间建立一种一一映射的关系。那么就可以在查找时快速的找到需要的元素。
哈希概念
哈希之散列方法:
插入元素时:根据需要插入元素的值,通过某种计算得出元素的存储位置,将该元素插入到其对应的位置。
查找元素时:根据需要查找的元素进行某种计算得到其存储位置,将该位置的元素与查找的元素进行比较,若相同则查找成功。
例如:数据集合为{180,750,460,430,800,600,541}
哈希函数:Hash(key) = key%m;(m为内存单元的个数)
假设在该例子中,m为12;
Hash(180) = 0;
Hash(750) = 6;
Hash(460) = 4;
Hash(430) = 10;
Hash(800) = 8;
Hash(603) = 3;
Hash(541) = 1;
所以这些数据集合在内存中的存储为:
可是如果数据有冲突 了应该怎么办???
哈希冲突
哈希冲突:对于值不相同的元素但是却有相同的哈希值。
例如:对于两个元素a,b并且a!=b
但是Hash(a) == Hash(b);
不同的元素通过相同的哈希函数得到相同的哈希地址。
引起哈希冲突的原因:哈希函数设计的不够合理。
哈希函数的设计原则:
如果哈希表允许容纳的元素个数为m,那么元素的值域为0~m-1。
哈希函数计算出来的地址尽量均匀的分布整个空间之中。
常见的哈希函数
直接定制法:
即取元素的某个线性函数为散列地址:Hash(key) = A*key +B;
例如:找出字符串中只出现一次的字符。时间复杂度为O(N),空间复杂度为:O(1);(就可以使用该方法,开辟一个256个元素的数组,进行统计每个元素出现的次数)
优点:简单,均匀
适合于查找比较小而且连续的情况。
除留取余法:(比较常用的方法)
Hash(key) = key % p;(p <= m && p 质数),m为散列表中允许的地址个数。
平方取中法:
对数据进行平方,然后取数据的中间3位为哈希地址。
适合于:不知道数据的分布情况,但是数字又不是很大的情况
若哈希函数设计的非常合理,那么产生哈希冲突的概率就非常低,但是哈希冲突是无法避免的。
哈希冲突的处理:
方法一:
闭散列(即开放地址法):当发生哈希冲突时,如果该哈希表还没有被填满,那么就把该元素放到哈希表的下一个空闲的位置。
线性探测法查找下一个位置:
例如:关键码集合为:{37,25,14,36,49,57,11},设表的长度为12,Hash(key) = key%p(p = 11);
Hash(37) = 4;
Hash(25) = 3;
Hash(14) = 3;
Hash(36) = 3;
Hash(49) = 5;
Hash(57) = 2;
Hash(11) = 0;
很明显:这组数据的哈希地址有冲突。
在插入时,如果该位置已经有元素了,就从该位置起向后找,找到一个空闲的位置就进行插入。
如下图所示:
优点:简单 易懂
缺点:一旦发生了哈希冲突,所有的冲突连接在一起,很容易产生数据”堆积”。即不同的数据占用可以利用的位置,就使得寻找其余数据的位置需要进行多次比较,就会导致查找的效率降低。
负载因子
散列表的负载因子的值为:α = 填入表中的元素个数 / 散列表的长度。
分析:由于表长是定值,那么α就与”填入表中的元素个数”成正比。所以,α越大,就说明填入表中的元素个数越多,那么产生冲突的可能性就越大;反之,α越小,就说明填入表中的越少,产生的冲突就越小,但是可能浪费的空间就越多。
对于开放地址法:负载因子特别重要,应该限制在07-0.8之内。若超过0.8,就可能产生冲突的概率非常大,那么CPU缓存不命中率也就越高。
二次探测法:
就是当有哈希冲突时,寻找下一个空闲位置时,首先在该位置处加1的平方,若加1的平方的位置处依然有元素,那就加2的平方,知道找到一个空闲的位置为止。
方法2:开散列法(哈希桶):又名链地址法,先用哈希函数计算每个数据的散列地址,把具有相同地址的元素归于同一个集合之中,把该集合处理为一个链表,链表的头节点存储于哈希表之中。
例如:还是上面闭散列中的例子,当使用开散列的方法后,其每个元素的存储为下图所示:
由此可见:开散列法有效的解决了数据溢出,不过需要增设链接指针,增加了存储的开销。但是,总体而言,效率还是快的多。
理想的查找是不经过任何比较就能根据所查关键吗直接得到待查记录所在的存储位置。散列查找技术就是朝该方向努力,它在关键码和存储位置之间建立一种对应关系,散列函数,由该函数可计算出关键码唯一的地址。在存储元素时通过计算通过计算关键码的散列函数值确定存储地址。查找时,计算给定关键字的散列函数值得到存储地址,直接查找。
关键码和存储位置之间的对应函数是散列函数,也称为哈希函数,hash函数。
一、散列函数的构造方法
1、直接定址法
关键码本身和地址之间存在某个线性函数关系时,散列函数取为关键码的线性函数,即:H(key)=a*key+b,a、b均为常数。
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中,直接定址法虽然简单,但却并不常用。
2、数字分析法
假设关键码完全已知,且每个关键码都是以某个数r为基数(例以10为基数的十进制数)的值,则关键码中若干位恰能构成分布比较均匀的散列地址空间时,可取关键码的若干位的组合作为散列地址。
3、除留余数法
通过选择适当的正整数p,按计算公式H(K)=Kmodp来计算关键码K的散列地址。
若关键码个数为n,散列表表长为m(一般m>=n),通常选p为小于或等于表长m的最大素数或不包含小于20的质因子的合数,一般也要求p>=n。
这种方法计算最简单,也不需根据全部关键码的分布情况研究如何从中析取数据,最常用。
4、平方取中法
将关键码K平方,取K^2中间几位作为其散列地址H(K)的值。
假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。
5、折叠法
将关键码从低位到高位(或从高位到低位)分割成位数相等的几段,最后一段可以短些,然后将这些段构成的数值按照某种叠加方法求和。最后,在散列地址范围限制下,取求和结果的最后几位作为关键码的散列函数值。
叠加方法:
(1)移位叠加:将各段数值最后一位对齐相加
(2)间界叠加:从各个数值段的一端到另一端来回折叠后(奇数段位正序,偶数为倒序),以最后一位对齐后相加
例:
key=12360324711202065,哈希表长度为1000,则应把关键字分成3位一段,在此舍去最低的两位65,分别进行移位叠加和折叠叠加,求得哈希地址为105和907,如图所示。
1 2 3 1 2 3
6 0 3 3 0 6
2 4 7 2 4 7
1 1 2 2 1 1
+) 0 2 0 +) 0 2 0
———————— —————
1 1 0 5 9 0 7
(a)移位叠加 (b) 间界叠加
6、随机数法
采用随机函数作为散列函数H(Key)=random(Key),其中random为随机函数。
当关键码长度不等时,采用该方法较恰当。
二、冲突的处理方法
1、开放定址法(建立闭散列表)
开放定址指散列表的地址对任何记录数据都是开放的,即可存储使用。但散列表长度一旦确定,总的可用地址是有限的。闭散列表表长不小于所需存储的记录数,发生冲突总能找到空的散列地址将其存入。查找时,按照一种固定的顺序检索散列表中的相应项,直到找到一个关键字等于k或找到一个空单元将k插入,故是动态查找结构。
1)线性探测法
从发生冲突位置的下一个位置开始寻找空的散列地址。发生冲突时,线性探测下一个散列地址是:Hi=(H(key)+di)%m,(di=1,2,3...,m-1)。闭散列表长度为m。它实际是按照H(key)+1,H(key)+2,...,m-1,0,1,H(key)-1的顺序探测,一旦探测到空的散列地址,就将关键码记录存入。
该方法会产生堆积现象,即使是同义词也可能会争夺同一个地址空间,今后在其上的查找效率会降低。
2)二次探测法
发生冲突时,下一位置的探测采用公式:Hi=(H(key)+di)%m,(di=1^2,-1^2,2^2,-2^2,.....,q^2,-q^2,q<=根号下m)
在一定程度上可解决线性探测中的堆积现象。
3)随机探测法
di为{1,2,3,...,m-1}中的数构成的一个随机数列中顺序取的一个数
4)再散列函数法
除基本散列函数外,事先设计一个散列函数序列,RH1,RH2,...,RHk,k为某个正整数。RHi均为不同的散列函数。对任一关键码,若在某一散列函数上发生冲突,则再用下一个散列函数,直到不发生冲突为止。
5)建立公共溢出区(单链表或顺序表实现)
另外开辟一个存储空间,当发生冲突时,把同义词均顺序放入该空间。若把散列表看成主表或父表,则公共的同义词表就是一个次表或子表。查找时,现在散列表中查,找不到时再去公共同义词子表顺序查找。
2、拉链法(链地址法、建立开散列表)
将所有散列地址相同的记录存储在同一个单链表中,该单链表为同义词单链表,或同义词子表。该单链表头指针存储在散列表中。散列表就是个指针数组,下标就是由关键码用散列函数计算出的散列地址。初始,指针数组每个元素为空指针,相当于所有单链表头指针为空,以后每扫描到一条记录,按其关键码的散列地址,在相应的单链表中加入含该记录的节点。开散列表容量可很大,仅受内存容量的限制。
例:具体的关键字列表为(19,14,23,01,68,20,84,27,55,11,10,79),则哈希函数为H(key)=key MOD 13。则采用除留余数法和链地址法后得到的预想结果应该为:
三、散列表上的查找
1、时间复杂度分析
查找成功时的平均查找长度ASLsucc、查找不成功时的平均查找长度ASLunsucc
1)具体闭散列表的查找效率分析
关键码{19,14,23,1,68,20,84,27,55,11,10,79},散列表长度m=16,除留余数法设计散列函数,H(key)=key%13,
(1)线性探测法处理冲突
只考虑关键码的比较,不考虑判空等操作,以下列出比较次数
散列地址 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
关键码 14 1 68 27 55 19 20 84 79 23 11 10
成功时 1 2 1 4 3 1 1 3 9 1 1 3
不成功时 12 11 10 9 8 7 6 5 4 3 2 1
ASLsucc=(1*6+2*1+3*4+4*1+9*1)/n=30/12=2.5
ASLunsucc=(1+2+...+12)/m=78/16=4.375
在ASLunsucc的计算中,假设每个地址被查到的概率相等(等于1/m=1/16),
(2)二次探测法
散列地址 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
关键码 27 14 1 68 55 84 19 20 10 23 11 79
成功时 3 1 2 1 2 3 1 1 3 1 1 5
不成功时 2 6 4 4 3 5 5 1 2 3 1 1 1
ASLsucc=(1*6+2*2+3*3+5*1)/n=24/12=2
ASLunsucc=(1*3+2*2+3*2+4*2+5*2+6*1)/m=37/16=2.3125
2)具体开散列表的查找效率分析
如上图拉链法处理冲突中的哈希表,
ASLsucc=(1*6+2*4+3*1+4*1)/n=21/12=1.75
ASLunsucc=(1*2+2*3+4*1)/m=12/16=0.75
一般情况下,处理冲突方法相同的散列表,其查找成功时的平均查找长度依赖于散列表的装填因子:a=表中填入的记录数/哈希表长度
a越小,发生冲突的可能性越小,反之,a越大,表中已填入记录越多,再填记录时,发生冲突的可能性越大,查找时给定值需与之进行比较关键码数目越多。
作者:zhangjiqun
来源:CSDN
原文:https://blog.csdn.net/qq_38998213