【数据结构与算法分析】散列相关知识点

1 散列的概念

散列是一种用于以常数平均时间执行插入、删除和查找的技术。但是,那些需要元素间任何排序信息的操作将不会得到有效的支持。
理想的散列表数据结构只不过是一个包含关键字的具体固定大小的数组。我们把表的大小记作TableSize,每个关键字被映射到从0到TableSize-1这个范围中的某个数,并且被放到适当的单元总。这个映射就叫做散列函数(hash function),理想情况下他应该运算简单并且应该保证任何两个不同的关键字映射到不同的单元。不过这是不肯的,因为单元的数目是有限的,而关键字实际上是用不完的。我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字。选择一个函数,决定当两个关键字散列到同一个值的时候(称为冲突)应该做什么以及如何确定散列表的大小。

2 散列函数

如果输入的关键字是整数,则一般合理的方法是直接返回“Key mod TableSize”的结果,除非Key碰巧具有某些不理想的性质。当一个元素被插入到另一个元素已经存在(散列相同),那么就产生一个冲突,这个冲突需要消除。通常的解决方法有两种:分离链接法和开放定址法。

3 分离链接法

解决冲突的第一种方法通常叫做分离链接法(separatechaining),其做法是将散列到同一个值的所有元素保留到一个表中。为方便起见,这些表都有表头。如果空间很紧,则更可取的方法是避免使用这些表头。我们假设关键字是前10个完全平方数并设散列函数就是Hash(X) =X mod 10, (表的大小不是素数,素数保证一个好的分布,用在这里是为了简单。)
在这里插入图片描述

4 开放定址法

分离链接散列算法的缺点是需要指针,由于给新单元分配地址需要时间,因此这就导致算法的速度多少有些减慢,同时算法实际上还要求对另一种数据结构的实现。除使用链表解决冲突外,开放定址散列法(Open addressing hashing)是另外一种用链表解决冲突的方法。在开放定址散列算法系统中,如果有冲突发生,那么就要尝试选择另外的单元,直到找出空的单元为止:更一般地,单元 h 0 ( X ) , h 1 ( X ) , h 2 ( X ) h_0(X) , h_1(X), h_2(X) h0(X),h1(X),h2(X),等等,相继被试选,其中 h i ( X ) = ( H a s h ( X ) + F ( i ) )   m o d    T a b l e s i z e h_i(X) =(Hash (X) +F(i))\ mod\ \ Tablesize hi(X)=(Hash(X)+F(i)) mod  Tablesize,且F(0) =0,函数F是冲突解决方法。因为所有的数据都要置入表内,所以开放定址散列法所需要的表要比分离链接散列用表大。

4.1 线性探测法

在线性探测法中,函数F是i的线性函数,典型情况是 F ( i ) = i F(i)=i F(i)=i。这相当于逐个探测每个单元(必要时可以绕回)以查找出一个空单元。

4.2 平方探测法

平方探测是消除线性探测中一侧聚集问题的冲突解决办法。平方探测就是冲突函数为二次函数的探测方法。流行的选择是 F ( i ) = i 2 F(i)=i^2 F(i)=i2

4.3 双散列

最后一个冲突解决方法是双散列(double hashing)。对于双散列,一种流行的选择是 F ( i ) = i ⋅ h a s h 2 ( X ) F(i) =i · hash_2(X) F(i)=ihash2(X)。这个公式是说,我们将第二个散列函数应用到X并在距离 h a s h 2 ( X ) , 2 h a s h 2 ( X ) hash_2 (X), 2hash_2(X) hash2(X),2hash2(X)等处探测。 h a s h 2 ( X ) hash_2(X) hash2(X)选择得不好将会是灾难性的。诸如 h a s h 2 ( X ) = R − ( X   m o d   R ) hash_2(X) =R-(X\ mod \ R) hash2(X)=R(X mod R)这样的函数将起到良好的作用,其中R为小于TableSize的素数。

4.4 几种散列函数的优缺点

分离链接:没有冲突,动态内存分配而昂贵。
线性探测:容易实现,但由于主聚类的负载因素增加,性能严重下降。
平方探测:也很容易实现。它的性能比线性探测好,但如果表中有超过一半的元素,则插入容易失败。
双散列:消除主和次级聚集,但是第二个哈希函数的计算代价很高。

一般来说平方探测法最好

5 再散列

对于使用平方探测的开放定址散列法,如果表的元素填得太满,那么操作的运行时间将开始消耗过长,且Insert操作可能失败。这可能发生在有太多的移动和插入混合的场合。此时,一种解决方法是建立另外一个大约两倍大的表(而且使用一个相关的新散列函数),扫描散列表,计算每个(未删除的)元素的新散列值并将其插入到新表中。
再散列显然是一种非常昂贵的操作;其运行时间为O(N),因为有N个元素要再散列而表的大小约为2N,不过,由于不是经常发生,因此实际效果根本没有这么差。特别是,在最后的再散列之前必然已经存在N/2次Insert,当然添加到每个插入上的花费基本上是一个常数开销。如果这种数据结构是程序的一部分,那么其效果是不显著的。另一方面,如果再散列作为交互系统的一部分运行,那么其插入引起再散列的不幸的用户将会感到速度减慢。
再散列可以用平方探测以多种方法实现。一种做法是只要表满到一半就再散列。另一种极端的方法是只有当插入失败时才再散列。第三种方法即途中(middle-of-thered)策略:当表到达某一个装填因子时进行再散列。由于随着装填因子的增加表的性能的确有下降,因此,以好的截止手段实现的第三种策略,可能是最好的策略。
再散列把程序员从表大小的担心中解放出来,这一点很重要,因为在复杂的程序中散列表不能够做得任意地大。

6 散列的应用

散列有着丰富的应用。编译器使用散列表跟踪源代码中声明的变量,这种数据结构叫做符号表(symbol table),散列表是这种问题的理想应用,因为只有Insert和Find要运行 标识符一般都不长,因此其散列函数能够迅速被算出。
散列表对于任何图论问题都是有用的,在图论问题中,节点都有实际的名字而不是数字。这里,当输入被读进的时候,顶点则按照它们出现的顺序从1开始指定为一些整数 再有,输入很可能有一组一组依字母顺序排列的项:例如,顶点可以是计算机。此时,如果一个特定的计算中心把它的计算机列表成为ibnml. ibm2, ibm3,等等,那么,若使用查找树则在效率方面可能会有戏剧性的效果。
散列表的第三种常见的用途是在为游戏编制的程序中,当程序搜索游戏的不同的行时.它跟踪通过计算基于位置的散列函数而看到的一些位置。如果同样的位置再出现,程序通常通过简单移动变换来避免昂贵的重复计算 游戏程序的这种一般特点叫做变换表(transpsition table)
散列的另一个用途是在线拼写检验程序。如果错拼检测(与正确性相比)更重要,那么整个目录可以被再散列,单词则可以在常数时间内被检测。散列表很适合这项工作,因为以字母顺序排列单词并不重要; 而以它们在文件中出现的顺序显示出错误拼写当然是可接受的。

猜你喜欢

转载自blog.csdn.net/baicoo/article/details/107369120