算法导论 第11章 散列表

散列表是主要支持动态集合的插入、搜索和删除等操作,其查找元素的时间在最坏情况下是O(n),但是在是实际情况中,散列表查找的期望是时间是O(1),散列表是普通数组的推广,因为可以通过元素的关键字对数组进行直接定位,所以能够在O(1)时间内访问数组的任意元素。

1、直接寻址表

当关键字的全域较小,即所有可能的关键字的量比较小时,可以建立一个数组,为所有可能的关键字都预留一个空间,这样就可以很快的根据关键字直接找到对应元素,这就是直接寻址表,在直接寻址表的查找、插入和删除操作都是O(1)的时间。、

2、散列表

在关键字的全域很大,可能的关键字很多的时候,要建立一个为所有可能的关键字都预留空间的数组是不现实的,特别是当实际要保存的关键字相比全域来说很少的时候就存在大量的空间浪费。这个时候就适合用散列表,其需要的存储空间要比直接寻址表小很多。虽然空间少了,但是散列表也可以保证在平均情况下元素的查找时间为O(1)。

在直接寻址表中,各个关键字就是数组中槽的序号,直接一一对应;但是在散列表中,最重要的就是散列函数,因为在散列表中,表空间只有实际会保存的元素的数量,这样散列函数根据各个元素的关键字来计算出它应该存放在散列表中的位置,这也就产生了一个问题,两个不同的关键字通过散列函数计算出的值可能相同,一个位置不可能存储两个元素,这时就必须解决这种碰撞问题。

解决碰撞问题有两方面的思路:一个就是设计一个好的散列函数,让尽可能少的关键字产生碰撞。另一个就是在实际的表空间比所有的可能的关键字少,或者甚至比实际要保存的关键字的个数都少,这时候必然出现碰撞,我们就必须设计一个方法处理出现的碰撞:链接法和开放寻址法。

2.1、散列函数

一个好的散列函数必须有很好的随机性,能够将每个关键字都等可能的散列到m(散列表大小)个表槽位的任何一个位置上,并与其他的关键字已被散列到哪一个槽位无关。所以散列函数需要独立于数据中可能存在的任何模式的方式导出散列值。下面列出一些常用的散列方法。

因为散列通常是将关键字放入散列函数中进行计算得到散列值,所以通常要将关键字转换成自然数,如果关键字是字符串,则可以将字符串按照适当的基数记号表示成整数。

除法散列

散列函数:h(k) = k mode m,m取与2的整数幂不太接近的质数较好,不宜取2的幂。

乘法散列

散列函数:h(k) = m*(k*A mod 1),其中0<A<1,m乘以 kA 的小数部分从而确定位置,A取黄金比例是个比较理想的值。

全域散列

散列函数是从一个特定设计的散列函数集合中随机的选择一个散列函数进行散列,这样即使是同一个关键字每一次散列的结果都不一样,这样保证了随机性,是散列表的平均性能比较好。

2.2、链接法

当使用散列表遇到冲突碰撞的时候,可以把散列到同一槽中的所有元素都放到一个链表中,这个槽就存放链表的头指针。使用链接法的散列表,在散列表的槽数与元素数量成正比关系的时候,每个槽中的链表的平均长度为O(1),因此在平均情况下,散列表查找、插入和删除的时间复杂度均为O(1)。

2.3、开放寻址法

不同于链接法,开放寻址法不适用链表,而是将所有的元素都存储于散列表的槽中,这还可以省去保存链表指针的存储空间,将这些空间来保存元素。在开放寻址法中,如果一个元素的关键字散列到的位置已经有元素了,则说明出现了碰撞,这时候散列函数会继续将元素散列到下一个位置,如果继续冲突,则继续探查,一次类推,直到找打空的槽来保存元素,因此对于每一个插入的元素,散列函数都会产生一个探查序列,依次探查各个位置寻找空槽,最多探查m(槽数)次。所以这里的散列函数不同于链接法的散列函数只是产生一个特定的散列值,开放寻址法中的散列函数都是产生一个探查序列。常用的方法有:线性探查、二次探查和双重探查。

线性探查

散列函数:h(k, i) = (h‘ (k) + i) mod m,i = 0, 1, 2, ... , m - 1,其中h‘(k)为辅助散列函数。

在线性探查过程中,每一次探查的偏移量为1,一旦h‘(k)确定则整个探查序列也就确定了,所以一共可以产生m中探查序列。此方法容易产生一次集群,因为探查序列式偏移量为1的连续空间,所以被占用的槽越多,碰撞的几率也会越大,查询时间也会增加。

二次探查

散列函数:h(k, i) = (h‘(k) + c1*i^2 + c2*i) mod m,其中h‘是辅助散列函数,c1和c2为辅助常数。

在二次探查中,探查序列的偏移量不是1,而是不断变化的(是一个确定的值序列),但是同线性探查一样,只要h‘(k)确定了,整个探查序列也就确定了,所以一共只有m种探查序列。

双重探查

散列函数:h(k, i) = (h1(k) + i*h2(k)) mod m,其中h1和h2是辅助散列函数

在双重探查中,探查序列的偏移量是不断变化的,而且每一个k值对应的偏移量变化序列都不一样,队以同一个h1(k)的值,可能对应m种不同的偏移量h2(k),所以共能产生m^2个探查序列。所以它是开放寻址法中最好的方法之一。但是为了能查找整个散列表,值h2(k)必须要与表的大小m互质,否则h2的值和m存在公约数,则探查序列的长度就小于m了。

开放寻址法在平均情况下,每个元素的探查次数都是O(1)。所以整体性能也就好。

2.4、完全散列

散列技术因其出色的期望性能而广泛使用,当关键之的集合是静态的时候,也就是关键字存入表中关键字集合就不在变化了,那么就可以使用完全散列,其最坏情况下搜索的时间复杂度能达到O(1),完全散列其实就是两级散列,第一级仍然是建立一个散列表,将n个关键字散列到m个槽中,第二级散列就是出现碰撞的时候不适用链接法和开放寻址法,而是在对应每个槽又建立一个散列表,将冲突的元素散列到同一个表中,并保证第二级散列不出现碰撞,为了保证这一点,第二级散列表的大小应为散列到该槽的元素个数的平方,看似需求很大空间,实际上总体期望空间仍然是O(n)。

时间: 2024-08-01 15:11:24

算法导论 第11章 散列表的相关文章

算法导论第11章散列表11.1直接寻址表

/* * IA_11.1DirectAddressTables.cpp * * Created on: Feb 11, 2015 * Author: sunyj */ #include <stdint.h> #include <iostream> #include <string.h> // DIRECT-ADDRESS-SEARCH(T, k) // return T[k] // DIRECT-ADDRESS-INSERT(T, x) // T[x.key] = x

算法导论第十一章 散列表

一.散列表的概念 本章介绍了散列表(or hash table)的概念.散列函数的设计及哈希冲突的处理.散列表(为了形象描述,我们通常叫槽)从表意上看是一种数据结构,但把它归为算法思想更为贴切.对于大部分的查找问题,使用散列表能达到O(1)的效率.现在很多大公司在面试大数据的题目时,解决方案里绝对少不了散列表的思想,例如百度的一道面试题:Top K查找问题: 问题描述: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节. 假设目前有一千万个记录(这

【算法导论】学习笔记——第11章 散列表

11.1 直接寻址表当关键字的全域U很小,可采用直接寻址的方式.假设动态集合S的元素都取自全域U={0, 1, ..., m-1}的一个关键字,并且没有两个元素具有相同的关键字.为表示动态集合,使用直接寻址表(diret-address table),记为T[0...m-1],其中的每个位置称为槽(slot).直接寻找表就是按照数组索引,缺点明显.基本操作如下: 1 DIRECT-ADDRESS-SEARCH(T, k) 2 return T[k] 3 4 DIRECT-ADDRESS-INSE

《算法导论》读书笔记之第11章 散列表

本章介绍了散列表(hash table)的概念.散列函数的设计及散列冲突的处理.散列表类似与字典的目录,查找的元素都有一个key与之对应,在实践当中,散列技术的效率是很高的,合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1).散列表是普通数组概念的推广,在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的.书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的,再次证明了数学真的很重要.在STL中map容器的功能就是散列表的功能,但是map

算法导论学习笔记——第11章 散列表

直接寻址表 1 DIRECT-ADDRESS-SEARCH(T,k) 2 return T[k] 3 4 DIRECT-ADDRESS-INSERT(T,x) 5 T[key[x]]←x 6 7 DIRECT-ADDRESS-DELETE(T,x) 8 T[key[x]]←nil

算法导论 第6章 堆排序(简单选择排序、堆排序)

堆数据结构实际上是一种数组对象,是以数组的形式存储的,可是它能够被视为一颗全然二叉树,因此又叫二叉堆.堆分为下面两种类型: 大顶堆:父结点的值不小于其子结点的值,堆顶元素最大 小顶堆:父结点的值不大于其子结点的值,堆顶元素最小 堆排序的时间复杂度跟合并排序一样,都是O(nlgn),可是合并排序不是原地排序(原地排序:在排序过程中,仅仅有常数个元素是保存在数组以外的空间),合并排序的全部元素都被复制到另外的数组空间中去,而堆排序是一个原地排序算法. 1.在堆排序中,我们通常使用大顶堆来实现,因为堆

算法导论 第7章 高速排序

高速排序在最坏情况下的时间复杂度为O(n^2),尽管在最坏情况下执行时间比較差,可是高速排序一般是用于排序的最佳选择.由于其平均性能相当好,期望的执行时间为O(nlgn),且在O(nlgn)的记号中隐含的常数因子非常小. 高速排序和合并排序有相似之处,都是须要划分序列,在合并排序中.划分的过程非常easy.直接选择元素序列的中间位划分位置,排序是在合并的过程中实现的,所以合并排序的合并过程非常重要.相比合并排序,高速排序就没有合并的过程.仅仅有划分,高速排序的划分过程非常重要,排序是在划分的过程

算法导论 第7章 快速排序

快速排序在最坏情况下的时间复杂度为O(n^2),虽然在最坏情况下运行时间比较差,但是快速排序通常是用于排序的最佳选择,因为其平均性能相当好,期望的运行时间为O(nlgn),且在O(nlgn)的记号中隐含的常数因子很小. 快速排序和合并排序有相似之处,都是需要划分序列,在合并排序中,划分的过程很简单,直接选择元素序列的中间位划分位置,排序是在合并的过程中实现的,所以合并排序的合并过程很重要:相比合并排序,快速排序就没有合并的过程,只有划分,快速排序的划分过程很重要,排序是在划分的过程中实现的. /

算法导论 第2章

本章主要是算法知识的基础讲解,介绍了循环不变式,几个简单的排序算法,递归分治算法等内容. 1.循环不变式 循环不变式主要用来说明算法的正确性,那么什么是循环不变式呢,其实就是在循环过程中,一些元素数据必须保持的一些性质,例如在插入排序中,数组为A,必须保证三个性质: (1) 初始化:在循环开始之前,循环不变式是成立的,即:A[0]是有序的,A[1...n-1]是无序的. (2) 保持:在循环的某一次迭代开始之前,循环不变式是成立的,那么在此次迭代结束后依然应该是成立的,即:A[0...i]是有序