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. iOS 将navigationItem.titleView设置为自定义UISearchBar (Ficow实例讲解)

    这篇文章可以解决以下问题: 1.将searchBar设置为titleView后,无法调整位置的问题 : 2.searchBar的背景色无法设置为透明色的问题: 3.searchBar输入框内用户输入的 ...

  2. 2017百度之星资格赛 1003:度度熊与邪恶大魔王(DP)

    .navbar-nav > li.active > a { background-image: none; background-color: #058; } .navbar-invers ...

  3. (028)[技术资料]et99加密狗打开函数的一个小bug

    et99加密狗的打开函数,其官方vb调用申明如下:Declare Function et_OpenToken Lib "FT_ET99_API.dll" (ByRef et99ha ...

  4. Suricata的规则解读(默认和自定义)

    不多说,直接上干货! 见suricata官网 https://suricata.readthedocs.io/en/latest/rules/index.html 一.Suricata的规则所放位置 ...

  5. Clean Code 第十章 : 类

    最近的CleanCode读到了第十章.这一张主要讲了如何去构造一个类,感觉的CleanCode至此已经不仅仅是单纯的讲如何'写'出漂亮的代码,而是从设计方向上去构造出好的代码了. 本章节主要讲了: * ...

  6. 在docker容器中运行hello world!

    在docker容器中运行hello world! docker容器可以理解为在沙盒中运行的进程.这个沙盒包含了该进程运行所必须的资源,包括文件系统.系统类库.shell 环境等等.但这个沙盒默认是不会 ...

  7. 64位系统上32位进程拷贝文件到System32目录时的重定向

    64位系统上,32位进程拷贝文件到"System32"目录时,会被文件系统重定向到"SysWOW64"目录 要禁用这种重定向,需要用到下面2个API: Wow6 ...

  8. phpstorm 格式化代码

    MAC 安装phpcs.phpcbf composer global require 'squizlabs/php_codesniffer=*' Changed current directory t ...

  9. 简单修改BOOK主题样式

    body{ font-size: 13px; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; margin: 0px; word ...

  10. SEO 第二章

    SEO第二章 1.  掌握搜索引擎工作原理(重点) 2.  了解百度算法 3.  关键词的分类 一.什么是搜索引擎? 搜索引擎是用来实现搜索服务的,说白了搜索引擎也属于一种网站. 浏览器是用来加载网站 ...