动态哈希

动态hash方法之一

本文将介绍三种动态hash方法。

散列是一个非常有用的、非常基础的数据结构,在数据的查找方面尤其重要,应用的非常广泛。然而,任何事物都有两面性,散列也存在缺点,即数据的局部集中性会使散列的性能急剧下降,且越集中,性能越低。

数据集中,即搜索键在通过hash函数运算后,得到同一个结果,指向同一个桶,这时便产生了数据冲突。

通常解决数据冲突的方法有:拉链法(open hashing)和开地址法(open addressing)。拉链法我们用的非常多,即存在冲突时,简单的将元素链在当前桶的最后元素的尾部。开放地址法有线性探测再散列、二次线性探测再散列、再hash等方法。

以上介绍的解决冲突的方法,存在一个前提:hash表(又称散列表)的桶的数目保持不变,即hash表在初始化时指定一个数,以后在使用的过程中,只允许在其中添加、删除、查找元素等操作,而不允许改变桶的数目。

在实际的应用中,当hash表较小,元素个数不多时,采用以上方法完全可以应付。但是,一旦元素较多,或数据存在一定的偏斜性(数据集中分布在某个桶上)时,以上方法不足以解决这一问题。我们引入一种称之为动态散列的方法:在hash表的元素增长的同时,动态的调整hash桶的数目。

动态hash不需要对hash表中所有元素进行再次插入操作(重组),而是在原来基础上,进行动态的桶扩展。有多种方法可以实现:hash表、可扩展的动态散列和线性散列,下面分别介绍之,方法由简单到复杂。

hash表:顾名思义,即采用多个hash表的方式扩展原hash表。这种方式不复杂,且理解起来也较简单,是三者中最简单的一种。

通常,当一个hash表冲突较多时,需要考虑采用动态hash方式,来减小后续操作继续在该桶上的冲突,减轻该桶负担,最简单且最容易想到的就是采用多hash表的方式。如下图,有一个简单的hash结构:

简单起见,假定(1)hash函数采用模5,即hash(i)=i%5;(2)每个桶中最多只可放4个元素。

在以上基础上,向hash表中插入5,由于桶a存在空闲,直接存入。接着向hash表插入值3,由于d桶中已满,无空闲位置,此时在建立一个hash表,结果如下图


    通过图示,一目了然,原来的一个hash表,变为现在的两个hash表。如需要,该“分裂”可继续进行。

需要注意的是:采用这种方式,多个hash表公用一个hash函数,且目录项的个数也随之增多,分别指向对应的桶。实际上,这时存在两个不同的目录项,分别指向各自的桶。

执行插入、查找、删除操作时,均需先求得hash值x。插入时,得到当前的hash表的个数,并分别取得各个目录项的x位置上的目录项,若其中某个项指向的桶存在空闲位置,则插入之。同时,在插入时,可保持多个hash表在某个目录项上桶中元素的个数近似相等。若不存在空闲位置,则简单的进行“分裂”,新建一个hash表,如上图所示。

查找时,由于某个记录值可能存在当前hash结构的多个表中,因此需同时在多个目录项的同一位置上进行查找操作,等待所有的查找结束后,方可判定该元素是否存在。由于该种结构需进行多次查找,当表元素非常多时,为提高效率,在多处理器上可采用多线程,并发执行查找操作。

删除操作,与上述过程基本类似,不赘述。需要注意的是,若删除操作导致某个hash表元素为空,这时可将该表从结构中剔除。

这种解决hash冲突的方法,优点是:思想简单,实现起来也不复杂。由于一存在桶满的情况就另分配一个hash表,因此占用内存空间较大;当数据较集中时,桶利用率会很低。

可扩展的动态散列:引入一个仅存储桶指针的目录数组,用翻倍的目录项数来取代翻倍的桶的数目,且每次只分裂有溢出的桶,从而减小翻倍的代价。这里需要几个参数:H表示hash函数、D表示全局位深度、L表示桶的局部深度。还是来看个例子,参照这个图,你会更明了。


图中,每个目录项有一个指向桶的指针,为介绍方便,我们假定(1)每个桶只可存放4个元素;(2)每个桶中存放的元素j*表示H(i)=j,为方便起见,只图示了j的值,并以’ *’标注。当前有4个目录项,目录项的编号从00~11,用两个位即可表示所有的目录项,因此全局位深度D=2;所有的桶目前最多只可放4个元素,因此所有桶的局部位深度为L=2。

在上图的基础上,我们插入数据d1和d2,且假定,经过hash函数求值后分别得到H(d1)=13;H(d2)=20。因为13=1101,因全局位深度为2,故选用最后两位01,找到编号为01的目录项,从而找到其指向的桶b,由于该桶还有空间,可直接存入数据。因20=10100,全局位深度为2,选用最后两位00,选定第一个目录项,这时我们发现其指向的桶a中已经放满了数据,于是该桶进行分裂,分裂的桶的局部位深度从2变为3,若这个数据比全局位深度还大,则全局位深度也等于该数,并进行目录项的翻倍操作。分裂的桶中的所有数据,需进行局部的重组。下图列出了分裂后的hash表的情况。


对a桶进行分裂后,得到两个桶a1和a2,其局部深度加1。由于局部深度大于全局位深度,因此目录数组进行翻倍,从4变为8,且目录编号扩展一位(如图)。桶a分裂为a1桶和a2桶,分别设置指针。对原来a桶中的所有元素进行重组操作,32和16的后三位均为000,于是放入a1桶,4和12的后三位均为100,于是放入a2桶。对目录项数组中其它未赋值的目录项,进行赋值,使指针指向对应的桶。至此,插入操作完毕。可以看到,有多个目录指向同一个桶。

对于查找操作,步骤如下:

1、对于需要查找的x,hash(x) = y

2、根据当前hash表的全局位深度,决定对y取其后D位,位数不够用0填充

3、找到对应的目录项,从而找到对应的桶,在桶中逐一进行比较。

对于删除操作,和查找操作类似,先定位元素,删除之。若删除时发现桶为空,则可以考虑将该桶与其兄弟桶进行合并,并使局部位深度减1。

可扩展散列的好处在于可动态进行桶的增长,且增长的同时,用目录项的翻倍的较小的代价换取桶数翻倍的传统做法,效率得到提升。然而,它也存在一定问题:(1)当散列的数据分布不均或偏斜较大时,会使得目录项的数目很大,数据桶的利用率很低;(2)目录的增长速度,是指数级增长,扩展较快。

动态hash思想方法之二

======》接动态hash方法之一

动态hash方法之二

线性散列:动态hash常用的另一种方法为线性散列,它能随数据的插入和删除,适当的对hash桶数进行调整,与可扩展散列相比,线性散列不需要存放数据桶指针的专门目录项,且能更自然的处理数据桶已满的情况,允许更灵活的选择桶分裂的时机,因此实现起来相比前两种方法要复杂。

理解线性散列,需要引入“轮转分裂进化”的概念,各个桶轮流进行分裂,当一轮分裂完成之后,进入下一轮分裂,于是分裂将从头开始。用Level表示当前的“轮数”,其值从0开始。假定hash表初始桶数为N(要求N是2的幂次方),则值logN(以2为底)是指用于表示N个数需要的最少二进制位数,用d0表示,即d0=logN。

以上提到,用Level表示当前轮数,则每轮的初始桶数为N*2^Level个(2^Level表示2的Level次方)。例如当进行第0轮时,level值为0,则初始桶数为N*2^0=N。桶将按桶编号从小到大的顺序,依次发生分裂,一次分裂一个桶,这里我们使用Next指向下次将被分裂的桶。

每次桶分裂的条件可灵活选择,例如,可设置一个桶的填充因子r(r<1),当桶中记录数达到该值时进行分裂;也可选择当桶满时才进行分裂。

需要注意的时,每次发生分裂的桶总是由Next决定,与当前值被插入的桶已满或溢出无关。为处理溢出情况,可引入溢出页解决。话不多说,先来看一个图示:


假定初始时,数据分布如上,hash函数为h(x)。桶数N=4,轮数Level为0,Next处于0位置;采用“发生溢出分裂”作为触发分裂的条件。此时d=logN=2,即使用两个二进位可表示桶的全部编号。

简单解释一下,为什么32*、25*、18*分别位于第一、二、三个桶中。因为h(x)=32=100000,取最后两个二进制位00,对应桶编号00;h(y)=25=11001,取最后两个二进制位01,对应桶编号01;h(z)=18=10010,最后两位对应桶编号10。

接下来,向以上hash表中插入两个新项h(x1)=43和h(x2)=37,插入结果如下图所示:


我们来分析一下。当插入h(x1)=43=101011时,d值为2,因此取末尾两个二进制位,应插入11桶。由于该桶已满,故应增加溢出页,并将43*插入该溢出页内。由于触发了桶分裂,因此在Next=0位置上(注意不是在11桶上),进行桶分裂,产生00桶的映像桶,映像桶的编号计算方式为N+Next=4+0=100,且将原来桶内的所有元素进行重新分配,Next值移向下一个桶。

当插入h(x2)=37=100101时,d值仍为2,取末尾两个二进制位,应插入01桶,该桶中有空余空间,直接插入。

分析到这里,读者应该基本了解了线性散列的分裂方式。我们发现,桶分裂是依次进行的,且后续产生的映像桶一定位于上一次产生的映像桶之后。

读者不妨继续尝试插入h(x)=29,22,66,34,50,情况如下图所示,这里不再详细分析。

线性散列的查找操作,例如要查询h(x)=18,32,44。假定查询时,hash表状态为N=4,Level=0,Next=1,因此d值为2。

(1) 查找h(x)=18=10010,取末两位10,由于10位于Next=1和N=4之间,对用桶还未进行分裂,直接取10作为桶编号,在该桶中进行查找。

(2) 查找h(x)=32=10000,取末两位00,由于00不在Next=1和N=4之间,表示该桶已经分裂,再向前取一位,因此桶编号为000,在该桶中进行查找。

(3) 查找h(x)=44=101100,取末两位00,由于00不在Next=1和N=4之间,表示该桶已经分裂,再向前取一位,因此桶编号为100,在该桶中进行查找。

线性散列的删除操作是插入操作的逆操作,若溢出块为空,则可释放。若删除导致某个桶元素变空,则Next指向上一个桶。当Next减少到0,且最后一个桶也是空时,则Next指向N/2 -1的位置,同时Level值减1。

线性散列比可扩展动态散列更灵活,且不需要存放数据桶指针的专门目录项,节省了空间;但如果数据散列后分布不均匀,导致的问题可能会比可扩展散列还严重。

至此,三种动态散列方式介绍完毕。

:对于多hash表和可扩展的动态散列,桶内部的组织,可采用(1)链式方法,一个元素一个元素的链接起来,则上例中的4表示最多只能链接4个这样的元素;也可采用(2)块方式,每个块中可放若干个元素,块与块之间链接起来,则上例中的4表示最多只能链接4个这样的块。

转载请注明原处:http://blog.sina.com.cn/weicb

时间: 2024-10-30 20:50:16

动态哈希的相关文章

增加桶式的动态哈希

哈希值的获取字符串的二进制,再二进制转换为一组数字,这组数字就是用来确定key在哪个桶层中的 代码中  #define tongsize 8 //桶大小是2的n次方,,,,,这里的n=3(2的3次方等于8),那么会把3个二进制转换为一个数字 /* 当重新分配还是分配到一个桶层时候,会出现哈希key不够用,就会出现引用了无效内存的bug,所以使用more_ceng arr[j]=strtol( bi, NULL, 2);//把二进制字符串bi转换为十进制 测试数据 set 电风 111 set 风

【数据结构】哈希表的线性探测算法

构造哈希表常用的方法是: 除留余数法--取关键值被某个不大于散列表长m的数p除后的所得的余数为散列地址.HashKey= Key % P. 直接定址法--取关键字的某个线性函数为散列地址HashKey= Key 或 HashKey= A*Key + BA.B为常数. 我在这里主要使用一下除留余数法Hash(key) =Key%P,(P这里是哈希表的长度)p最好是素数考虑降低哈希冲突的原因,我并没有在这上面过于追究此处哈希表长度10,见线性探测图. 哈希表经常遇到的一个问题就是哈希冲突. 哈希冲突

哈希函数和哈希表综述 (转)

哈希表及哈希函数研究综述 摘要 随着信息化水平的不断提高,数据已经取代计算成为了信息计算的中心,对存储的需求不断提高信息量呈现爆炸式增长趋势,存储已经成为急需提高的瓶颈.哈希表作为海量信息存储的有效方式,本文详细介绍了哈希表的设计.冲突解决方案以及动态哈希表.另外针对哈希函数在相似性匹配.图片检索.分布式缓存和密码学等领域的应用做了简短得介绍 哈希经过这么多年的发展,出现了大量高性能的哈希函数和哈希表.本文通过介绍各种不同的哈希函数的设计原理以及不同的哈希表实现,旨在帮助读者在实际应用中,根据问

SQL连接操作符介绍(循环嵌套, 哈希匹配和合并连接)

今天我将介绍在SQLServer 中的三种连接操作符类型,分别是:循环嵌套.哈希匹配和合并连接.主要对这三种连接的不同.复杂度用范例的形式一一介绍. 本文中使用了示例数据库AdventureWorks ,下面是下载地址:http://msftdbprodsamples.codeplex.com/releases/view/4004 简介:什么是连接操作符 连接操作符是一种算法类型,它是SQLServer优化器为了实现两个数据集合之间的逻辑连接选择的操作符.优化器可以基于请求查询.可用索引.统计信

内存数据库中的索引技术

引言 传统的数据库管理系统把所有数据都放在磁盘上进行管理,所以称作磁盘数据库(DRDB: Disk-Resident Database).磁盘数据库需要频繁地访问磁盘来进行数据的操作,磁盘的读写速度远远小于CPU处理数据的速度,所以磁盘数据库的瓶颈出现在磁盘读写上. 基于此,内存数据库的概念被提出来了.内存数据库(MMDB:Main Memory Database,也叫主存数据库)[1],就是将数据全部或者大部分放在内存中进行操作的数据库管理系统,对查询处理.并发控制与恢复的算法和数据结构进行重

Informix 11.5 SQL 语句性能监控方法及实现

我们知道,在数据库应用系统中,SQL 语句的性能好坏至关重要.如果 SQL 语句性能很差,可能会导致整个数据库应用系统的性能也非常差.那么,如何监控数据库系统中 SQL 语句的性能,导致 SQL 语句性能差的原因是什么? SQL 语句运行过程中对系统资源的使用情况如何?系统资源存在哪些瓶颈?在 Informix 11.5 中,主要提供了两个工具来解决上述问题.一个是 set explain 命令,我们可以通过查看数据库的查询计划来分析导致 SQL 语句性能差的原因并给予相应的调整,另一个是 SQ

MySQL Memory 存储引擎浅析

原创文章,转载必需注明出处:http://www.cnblogs.com/wu-jian/ 前言 需求源自项目中的MemCache需求,开始想用MemCached(官方站点:http://memcached.org/ ),但这个在Linux下面应用广泛的开源软件无官方支持的Windows版本.后来看到博客园在用NorthScale Memcached Server(官方站点:http://www.couchbase.com/products-and-services/memcached),貌似共

mysql数据库优化五步走

MySQL数据库是一种小型关系型数据库管理系统,MySQL数据库的优化是MySQL数据库操作过程中非常重要的工作,MySQL数据库的优化能够实现MySQL数据库操作的简便. 第一步: 1:磁盘寻道能力,以高速硬盘(7200转/秒),理论上每秒寻道7200次.这是没有办法改变的,优化的方法是----用多个硬盘,或者把数据分散存储. 2:硬盘的读写速度,这个速度非常的快,这个更容易解决--可以从多个硬盘上并行读写. 3:cpu.cpu处理内存中的数据,当有相对内存较小的表时,这是最常见的限制因素.

[论文]内存数据库中的索引技术

原创性申明 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/42490335 转载请注明出处 引言 传统的数据库管理系统把所有数据都放在磁盘上进行管理,所以称作磁盘数据库(DRDB: Disk-Resident Database).磁盘数据库需要频繁地访问磁盘来进行数据的操作,磁盘的读写速度远远小于CPU处理数据的速度,所以磁盘数据库的瓶颈出现在磁盘读写上. 基于此,内存数据库的概念被提出来了.内存数据库(MMDB:Main Memory