1 序

在很多应用中,都要用到一种动态集合结构,它仅支持INSERT、SEARCH以及DELETE三种字典操作。例如计算机程序设计语言的编译程序需要维护一个符号表,其中元素的关键字为任意字符串,与语言中的标识符相对应。实现字典的一种有效数据结构为散列表。

散列表是普通数组的推广,因为可以对数组进行直接寻址,故可以在O(1)的时间内访问数组的任意元素。对于散列表,最坏情况下查找一个元素的时间与在链表中查找的时间相同,为O(n),但是在实践中,散列表的效率通常是很高的,在一些合理的假设下,散列表中查找的期望时间为O(1)。

2 直接寻址表

当关键字的全域U比较小时,直接寻址是一种简单而有效的技术。

为表示动态数组,定义一个数组(直接寻址表)T[0…m-1],其中每个位置对于全域U中的一个关键字。具体方法如下图所示:



字典操作实现伪码:

//查询操作
DirectAddressSearch(T , k)
return T[k]; //插入操作
DirectAddressInsert(T , x)
T[key[x]] = x; //删除操作
DirectAddressDelete(T , x)
T[key[x]] = NULL;

对于直接寻址表时间复杂度很低,但是其存在的问题是,需要覆盖全域的内存容量,空间复杂度高。因此,对于全域U很大而且内存容量不足的应用问题,直接寻址表不是一个理想的解决方案。

3 散列表

在直接寻址表中,具有关键字k的元素就被放到相应的槽k中。在现在所讨论的散列表中,关键字k的元素的映射位置将由一个散列函数h(k)计算得到。



显然的,采用此种方法,内存空间的占用从全域|U|减少到关键字的个数m,大大节省了内存开销。

但是,这样做会带来问题,两个或多个关键字可能会映射到同一个槽上,也就是所说的碰撞冲突。这与散列函数(下一节介绍)的选取息息相关,当然,我们不仅需要通过精心设计的随机散列函数来减少碰撞,也需要思考找到解决有可能出现碰撞的办法。接下来详细介绍几种散列函数与碰撞冲突解决策略。

4 散列函数

散列函数h(k)是计算关键字k映射位置的一种函数。对于全域U中的m个关键字,一个好的散列函数应该近似的满足简单一致散列的假设:每个关键字等可能的散列到m个槽位的任何一个中去,并与其他的关键字映射到哪个槽位无关。在实践中,通常运用启发式技术来构造好的散列函数,一种好的做法是以独立于数据中可能存在的任何模式的方式导出散列值,例如接下来介绍的“除法散列”。

最开始介绍的直接寻址表也是一种散列方式,其h(k)=k,除此之外,下面介绍其它三种散列方式。

4.1 除法散列法

除法散列法是通过关键字k除以m的余数,来将k映射到m个槽的某一个中去,即散列函数为:

h(k)=kmodm

应用除法散列方式的关键在于m的选择。

4.2 乘法散列法

构造散列函数的乘法方法包括两个步骤:

第一步,用关键字k乘以常数A(0 < A < 1)并取出kA的小数部分;

第二步,用m乘以求出的小数部分,在去结果的底值;

散列函数为:

h(k)=floor(m(kAmod1))

乘法散列的一个优点是其对m的选择没有特殊要求,一般设为2的某个次幂。

4.3 全域散列

以上讨论的散列方法都不可避免的会出现最坏情况,即所有关键字映射到同一个槽内,这是平均检索时间为O(n)。其实,任何一个特定的散列函数都可能出现最坏情况,唯一有效的改进方法为随机的选取散列函数,使之独立于要存储的关键字,这种方法也被称为全域散列,该方法的平均性能最佳。

全域散列性态讨论详见《算法导论》P139~P141。

5 碰撞冲突解决策略

5.1链接法

链接法是一种最简单的碰撞解决技术,该方法选择把散列到同一个槽中的元素都放在一个链表中。例如槽j中有一个指针指向所有散列到j的元素构成的链表的头,如果没有元素映射到此,则该指针为nil。



链接法解决碰撞冲突后,散列表T上的字典操作就很容易实现了。

这里写代码片

由以上讨论可以看出,链接法解决冲突后,对于散列表的插入操作始终可以在O(1)内实现,查找操作时间复杂度与该元素所在链表的长度成线性关系,而删除操作对于双向链表删除一个元素x(指针结点)同样可以在O(1)实现,若是单链表必须首先根据输入参数查找目标结点的前一个结点,故其与查找操作的复杂度相同。

5.2 开放寻址法

开放寻址法也是一种碰撞冲突解决策略,在该方法中,所有的元素都存放在散列表里。在开放寻址法中,当要插入一个元素时,可以连续的检查或称为探查散列表的各项,直到找到一个空槽来放置待插入的关键字时为止。

这样,散列函数就变为:

h : U X {0,1,…m-1} -> {0,1,…m-1}

对于开放寻址法来说,要求每一个关键字k,探查序列< h(k,0) , h(k,1) , … , h(k ,m-1)>必须是< 0,1, … , m-1>的一个排列,使得当散列表被逐渐填满时,每一个表位最终都可以被视为用来插入新关键字的槽。

三种技术常用来计算开放寻址法中的探查序列:线性探查、二次探查以及双重探查。

i 线性探查

给定一个普通的散列函数h′:U−>0,1,...,m−1(称为辅助散列函数),线性探查方法采用的散列函数为:

h(k,i)=(h′(k)+i)modm,i=0,1,...m−1

给定一个关键字k,第一次探查的槽是T[h′(k)]也就是辅助散列函数给出的槽,若出现冲突,则接下来探查的槽为T[h′(k)+1],… ,直到T[m−1],然后又卷绕到T[0)],T[1],…,直到最后探查到T[h′(k)−1]。在线性探测方法中,初始探查位置决定了整个序列,故只有m种不同的序列。

线性探查方法简单,容易实现,但是其存在着一个问题—一次群集问题。随着时间的推移,被占用的槽也不断增加,因此,平均查找时间也会不断增加,群集现象很容易出现。

ii 二次探测

二次探查采用下面形式的散列函数:

h(k,i)=(h′(k)+c1∗i+c2∗i2)modm

其中h′是一个辅助散列函数,c1和c2是辅助常数,i=0,1,...,m−1。初始探查位置为T[h′(k)],后续探查位置由上面散列函数计算。这种方法比上面的线性探测效果有一定程度的提升,但是,对于二次探查,如果两个关键字的初始探查位置相同,则其探查序列必然相同,这是因为h(k1,0)=h(k2,0)意味着h(k1,i)=h(k2,i),这一性质导致一种轻度的群集现象称为二次群集。

iii 双重散列

双重散列是用于开放寻址法的最好方法之一,因为它所产生的排列具有随机选择的排列的许多特性,散列函数如下:

h(k,i)=(h1(k)+ih2(k))modm

其中h1和h2是辅助散列函数。初始探查位置为T(h1(k)),而后续的探查位置在此基础上加上偏移量h2(k)模m。

如下图所示:



与线性探查、二次探查不同的是,这里的探查序列以两种方式依赖于关键字k,因为初始探查位置、偏移量都有可能发生变化。

5.3完全散列

在本章的最后,讨论了一种高效的散列技术—完全散列,它在进行查找时,最坏情况内存访问次数为O(1)。实现思想如下图所示:

《算法导论》— Chapter 11 散列表的更多相关文章

  1. 基于visual Studio2013解决算法导论之028散列表开放寻址

     题目 散列表 解决代码及点评 #include <iostream> #include <time.h> using namespace std; template & ...

  2. Java数据结构和算法(一)散列表

    Java数据结构和算法(一)散列表 数据结构与算法目录(https://www.cnblogs.com/binarylei/p/10115867.html) 散列表(Hash table) 也叫哈希表 ...

  3. C++11散列表

    [C++11散列表] 散列表对应于C++03中的hash_xxx,分为set和map两种 上述的类型将满足对一个容器类型的要求,同时也提供访问其中元素的成员函数: insert, erase, beg ...

  4. Java数据结构与算法解析(十二)——散列表

    散列表概述 散列表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值. 散列表的思路很简单,如果所有的键都是整数,那么就可以使用一个简单 ...

  5. 算法导论——lec 11 动态规划及应用

    和分治法一样,动态规划也是通过组合子问题的解而解决整个问题的.分治法是指将问题划分为一个一个独立的子问题,递归地求解各个子问题然后合并子问题的解而得到原问题的解.与此不同,动态规划适用于子问题不是相互 ...

  6. (搬运)《算法导论》习题解答 Chapter 22.1-1(入度和出度)

    (搬运)<算法导论>习题解答 Chapter 22.1-1(入度和出度) 思路:遍历邻接列表即可; 伪代码: for u 属于 Vertex for v属于 Adj[u] outdegre ...

  7. 算法导论-散列表(Hash Table)-大量数据快速查找算法

    目录 引言 直接寻址 散列寻址 散列函数 除法散列 乘法散列 全域散列 完全散列 碰撞处理方法 链表法 开放寻址法 线性探查 二次探查 双重散列 随机散列 再散列问题 完整源码(C++) 参考资料 内 ...

  8. 散列表(hash table)——算法导论(13)

    1. 引言 许多应用都需要动态集合结构,它至少需要支持Insert,search和delete字典操作.散列表(hash table)是实现字典操作的一种有效的数据结构. 2. 直接寻址表 在介绍散列 ...

  9. 算法导论 第十章 基本数据类型 & 第十一章 散列表(python)

    更多的理论细节可以用<数据结构>严蔚敏 看几遍,数据结构很重要是实现算法的很大一部分 下面主要谈谈python怎么实现 10.1 栈和队列 栈:后进先出LIFO 队列:先进先出FIFO p ...

随机推荐

  1. 使用pytesseract识别验证码,报错WindowsError: [Error 2]

    问题现象: 按照网上的方式进行代码编写,使用pytesseract模块,然后导入指定图片进行解析,报错WindowsError: [Error 2] 问题原因: 源代码里面的路径设置错误,这里有一个坑 ...

  2. const 和 constexpr

    在C++中,const 这个关键字用法非常灵活,导致我总会搞不清作用是干啥的.灵活使用const会大大改善程序. const 是C++的一种类型修饰符,是不可改变的不能被更新的. 1.const 修饰 ...

  3. Oracle 单引号与双引号的区别

    双引号一般是用来转义的,如果alias里面有空格或其它保留符号,必须使用双引号.而单引号是用来特制的,比如字符串的引用,日期字符串的引用,都必须包括在单引号中,可以参与运算或其它表达式中.两者不可混用 ...

  4. ping localhost 返回 ::1的导致不能打开http://localhost的原因及解决

    虽然可以在浏览器中正常访问http://localhost但用file,file_get_contents等函数打开http://localhost异常.用127.0.0.1也可以打开,本地hosts ...

  5. C#之九大视图

    本节向大家介绍一下有关UML视图方面的内容,UML视图共有9种,它们之间有什么区别和联系呢,下面就让我们一起来学习吧,相信通过本节的介绍你一定会有不少收获. UML视图 UML总共提供了9种视图,这些 ...

  6. mysql索引命中规则

    转于:https://blog.csdn.net/claram/article/details/77574600 首先明确:为什么要用联合索引? 对于查询语句“SELECT E.* FROM E WH ...

  7. 关于重置功能(type="reset")的相关问题

    当一个按钮具有 type="reset";的按钮是具有重置表单标签的功能的,但是当具有type="hidden"; 属性的标签的值就不会被重置,这点要留意.可以 ...

  8. 5.iOS测试总结

    1. 什么是Mock 当我们在做单元测试的过程中,为了保持测试又短又快和测试的隔离性,希望尽可能少地去实例化一些具体的组件.在现在面向对象的系统中,被测试的对象很可能会依赖于几个其他的对象,这时候我们 ...

  9. C++模板类头文件和实现文件分离

    http://www.cnblogs.com/lvdongjie/p/4288373.html 如何实现C++模板类头文件和实现文件分离,这个问题和编译器有关. 引用<<C++primer ...

  10. 如何正确理解关键字"with"与上下文管理器

    转自:https://foofish.net/with-and-context-manager.html 如果你有阅读源码的习惯,可能会看到一些优秀的代码经常出现带有 “with” 关键字的语句,它通 ...