【MIT算法导论】哈希表、全域哈希

哈希表及哈希算法

一. 直接寻址

1. 应用条件
当关键字的全域比较小时(也即:候选的关键字的数目较少),直接寻址法才是行之有效的。

p.s. 借用下图,即U(全域)很小,那么需要存储的部分K(实际需要存储的关键字集合)只会更小,所以是可行的、

2. 结构定义
利用数组的底层结构即可直接构造出一个直接寻址表,如下图所示:
在这里插入图片描述
每个关键字作为数组的某一个下标(槽);槽中存储着或指向关键字为对应值的某个元素。
若某个关键字没有相对应的元素,则直接寻址表的这个槽中为空值。

注:上图中展示的数据结构中,每个元素的关键字和其卫星数据并不全在直接寻址表中;卫星数据放在表外的某一个对象中,由表中某个槽的指针指向该对象。

也可以直接把该对象放在表槽中,从而节省存储空间。

3. 操作方法
在这里插入图片描述


二. 散列表

1. 直接寻址技术的弊端

①若全域U很大,那么构建直接寻址表所需要的内存空间就很大,受限于计算机可用内存,是不可行的。

②若|K|<<|U|,即实际所需要存储的关键字的个数是远少于所有可能出现的关键字总数的,那么分配给直接寻址表的空间大部分都会被浪费。

2. 散列表机制

在平均情况下,散列表所需时间复杂度为O(1),所需空间复杂度为θ(|K|)

通过散列函数h,将关键字域U映射到散列表T[0,1,…,m-1]的m各槽位上。
h : U → 0 , 1 , . . . , m − 1 h:U→{0,1,...,m-1} h:U0,1,...,m1

  • 在直接寻址方式下,关键字k对应的元素存放在槽k中
  • 在散列方式下,关键字k对应的元素存放在槽h(k)中

在这里插入图片描述
3. 碰撞
(1)定义:两个关键字通过散列函数映射在同一个槽位上。

(2)解决措施:
①选用合适的散列函数h,使其尽可能地随机,从而避免或者最小化“碰撞”的产生。这也是“散列”这一操作的核心所在。

散列函数要尽可能地随机,但是随机函数是一个确定的函数,这两个概念不要混淆。
散列函数是确定的:某一个给定的输入k,应该始终产生相同的结果h(k)。

事实上,因为散列函数的定义,定义域是U,而值域是{0,1,…,m-1},因为**|U|>m**,所以必定存在两个或多个关键字有相同的散列值,所以无论怎样精心设计散列函数,我们都只能最小化而不能避免碰撞的发生。

②通过链接法解决碰撞的产生,也就是把散列到同一个槽中的所有元素放在一个链表中。后文对链接法会有详细的讨论与分析。

4. 链接法

(1)结构定义
在每一个槽中,都有一个对应的指针,指向一个链表的头部元素,该链表存储着所有散列到j的元素。
如果某个槽没有与之对应的元素,那么j即为NIL。
在这里插入图片描述
(2)操作方法

  • 插入某个元素x,则找到该元素对应的关键字key[x],计算其散列值h(key[x]),将这个元素放在T[h(key[x])]对应的链表中
  • 查找某个关键字k对应的元素,计算对应的散列值h(k),在链表T[h(k)]中进行元素的查找
  • 删除某个元素x,同样计算其关键字对应的散列值h(key[x]),然后在链表T[h(key[x])]中删除该元素x
    在这里插入图片描述

【插入操作的时间复杂度】
在假设待插入的元素不出现在链表的前提下,插入元素最坏情况复杂度也只有O(1)

如果抛开前提,在元素插入前需要进行元素的搜素以验证其是否在链表中,需要额外的时间代价。

【查找操作的时间复杂度】
复杂度与该槽中链表的长度成正比,具体分析见以下(3)部分

【删除操作的时间复杂度】
当链表是双链结构时,删除链表中给定的某一元素可以在O(1)时间内完成
当链表是单链结构时,因为删除一个元素需要找到该元素的前驱,所以依然需要对整个链表进行遍历,复杂度为O(n)

(3)散列法的分析
假定现在有一个散列表T,能存放n个元素,具有m个槽位(说明一共有n个可能出现的关键字,散列表只有m个空间),给出散列表的一个衡量因子:装载因子(load factor)α = n / m ,也即一个链中平均存储的元素数目。

①在最坏情况下
此时,全部的n个关键字经过散列到同一个槽中,产生了一个长度为n的链表,这时查找的时间为θ(n),相当于直接用一个链表来存储所有元素。

②在平均情况下

散列法的平均性能依赖于散列函数h的性能,也即取决于散列函数在一般情况下可以把所有关键字分布在m个槽位上的均匀程度

在这里讨论时,我们假定所有元素散列到任意一个槽中的可能性是相同的,且每一个元素被散列到何位置与其他元素是独立无关的(简单一致散列假设)。
对于j = 0,1,2…,m-1,槽j中对应的链表T[j]的长度设为nj,那么即可以得到
n = n 0 + n 1 + . . . + n m − 1 n = n_0+n_1+...+n_{m-1} n=n0+n1+...+nm1
其中可以知道,对于每一个槽中的链表T[j],其长度的期望值E[nj] = α = n / m

可以理解为,在简单一致散列假设下,相当于将n个元素平均分在m个槽中。

现考察“查找一个给定关键字k对应的元素”所需要的时间复杂度,设计算某一个关键字的散列值h(k)所需的时间复杂度为O(1);那么查找所需的时间线性依赖于T[h(k)]对应的链表的长度nh(k)

牵涉到查找操作的时间复杂度,就一定要分为查找成功与否进行讨论。
针对散列表,即使某个槽位中有多个对应的元素,也并不意味着其内存在着关键字为k的元素,因为不同的关键字可能会有相同的槽值,因此在遍历链表的过程中需要对键值进行比较。

  • 在查找不成功时,期望时间为θ(1+α)
    在这里插入图片描述
  • 在查找成功时,期望时间也为θ(1+α)
    在这里插入图片描述
    综合上述讨论,如果表中的元素数与散列表中的槽数成正比,则n = O(m),从而α = n/m = O(m)/m = O(1)。
    可下结论,在散列表的结构下,全部字典操作平均情况下都可以在O(1)的时间完成

三. 散列函数

1. 散列函数设计原则
(1)简单一致散列原则
综合来说,一个好的散列函数应该近似地满足[简单一致散列]的假设:每个关键字都等可能地散列到m个槽位的任何一个之中,并与其他的关键字被散列到哪一个槽位中无关。

但一般情况下,我们很少能知道关键字符合怎样的概率分布,而各个关键字之间也不会是完全互相独立的。所以我们无法检验该条件是否满足。

如果我们能知道关键字的概率分布情况,就可以通过分布信息来设计散列函数:
例:如果各关键字都是随机的实数k,它们独立地、一致地分布于范围0≤k<1之中,散列函数h(k) = (k·m)取下整即可满足简单一致散列原则。

(2)启发式技术
所谓启发式技术,就是利用有关关键字分布的限制性信息来设计散列函数。

比如在编译器的符号表中,已知关键字都是字符串且还可能会出现诸如“pt”、“pts”这样相近的字符串,那么在设计散列函数的时候就要尽可能地避免其二者被散列到同一个槽中。

(3)以独立于数据中可能存在的任何模式导出散列值。

比如“除法散列”中,选用一个特定的质数来除关键字,所得的余数作为散列值。
当选用的质数和关键字分布中的任何模式都无关,这一方法通常效果良好。

(4)其他比[简单一致散列假设]更强的原则,依照题意

比如说,会要求很近似的关键字有完全不同的散列值。

注:我们在讨论散列函数的时候,是假定关键字域为自然数籍N = {0,1,2,3…}。当所给的关键字不是自然数时,就要找到其和自然数之间的对应关系。

2. 除法散列法
在这里插入图片描述

在除数散列法中,对于除数的选择很重要。

3. 乘法散列法
在这里插入图片描述
4. 全域哈希

读者可以参考下述答案知乎-怎么理解全域哈希理解【全域哈希】的思想与实现

对于任意一个特定的散列函数,如果别有用心地设计一组关键字,那么会出现——该组的全部n个关键字会被全部散列到同一个槽中,使得平均检索时间退化到θ(n) 。

为了解决这个问题,需要随机地选择散列函数,使之独立于要存储的关键字。

(1)基本思想
在散列的执行开始时,从一族仔细设计的函数中,随机地选择一个作为散列函数(类似于快排中的随机化操作,它保证了快排运行中没有哪一种输入会始终导致最坏情况的性态)。
在这里插入图片描述

因为全域散列中引入了【随机化】的操作,使得即使是对同一个输入,算法每一次执行的形态也是不一样的。

(2)全域散列函数类的设计
在这里插入图片描述


四. 开放寻址法

1. 基本思想
在【开放寻址法】的构想中,所有元素都存放在散列表中——也就是说每个表项都应该包含动态集合中的一个元素或为NIL。

  • 查找一个元素时,需要检查所有的表项,直到找到所需的元素;
  • 插入一个元素时,可以连续地检查(探查)散列表的各个项,直到找到一个空槽来防止待插入的关键字

查找空槽的顺序不一定要按照0,1,…,m-1的顺序,可以依赖于待插入的关键字;我们可以将散列函数扩充成两个形参,第一个参数为关键字,第二个参数为探查号({0,1,…,m-1}),则散列函数形如:
h : U × { 0 , 1 , 2 , . . . , m − 1 } → { 0 , 1 , . . . , m − 1 } h:U×\left\{0,1,2,...,m-1\right\}→\left\{ 0,1,...,m-1\right\} h:U×{ 0,1,2,...,m1}{ 0,1,...,m1}
根据上文的描述,对于每一个关键字k,都会得到一个探查序列<h(k,0),h(k,1),…,h(k,m-1)>,这个序列应该是<0,1,…,m-1>的一个排列;它使得当散列表逐渐填满的时候,每一个表位最终都可以被用来插入新关键字的槽

  • 删除散列表的元素会较为麻烦,具体分析见下图:

在这里插入图片描述

下面会讲述三种常见的用来计算开放寻址法的探查序列的方法——线性、二次以及双重探查。

2. 线性探查
在这里插入图片描述
3. 二次探查
在这里插入图片描述
4. 双重探查
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/kodoshinichi/article/details/109628529