算法设计与分析——散列表/哈希表(Hash Table):散列函数

分类目录:《算法设计与分析》总目录
相关文章:
·散列表/哈希表(Hash Table)(一):基础知识
·散列表/哈希表(Hash Table)(二):直接寻址表
·散列表/哈希表(Hash Table)(三):散列表原理
·散列表/哈希表(Hash Table)(四):散列函数
·散列表/哈希表(Hash Table)(五):开放寻址法
·散列表/哈希表(Hash Table)(六):完全散列


本文将讨论一些关于如何设计好的散列函数的问题,并介绍三种具体方法。其中的两种方法(用除法进行散列和用乘法进行散列)本质上属于启发式方法,而第三种方法(全域散列)则利用了随机技术来提供可证明的良好性能。

好的散列函数的特点

一个好的散列函数应(近似地)满足简单均匀散列假设:每个关键字都被等可能地散列到 m m m个槽位中的任何一个,并与其他关键字已散列到哪个槽位无关。遗憾的是,一般无法检查这一条件是否成立,因为很少能知道关键字散列所满足的概率分布,而且各关键字可能并不是完全独立的。

有时,我们知道关键字的概率分布。例如,如果各关键字都是随机的实数 k k k,它们独立均匀地分布于 0 ≤ k ≤ 1 0\leq k\leq1 0k1范围中,那么散列函数
h ( k ) = ⌊ k m ⌋ h(k)= \lfloor km \rfloor h(k)=km
就能满足简单均匀散列的假设条件在实际应用中,常常可以运用启发式方法来构造性能好的散列函数。设计过程中,可以利用关键字分布的有用信息。例如,在一个编译器的符号表中,关键字都是字符串,表示程序中的标识符。一些很相近的符号经常会出现在同一个程序中,好的散列函数应能将这些相近符号散列到相同槽中的可能性最小化。

一种好的方法导出的散列值,在某种程度上应独立于数据可能存在的任何模式。例如,“除法散列”用一个特定的素数来除所给的关键字,所得的余数即为该关键字的散列值。假定所选择的素数与关键字分布中的任何模式都是无关的,这种方法常常可以给出好的结果。

最后,注意到散列函数的某些应用可能会要求比简单均匀散列更强的性质。例如,可能希望某些很近似的关键字具有截然不同的散列值。下文将介绍的全域散列通常能够提供这些性质。

将关键字转换为自然数

多数散列函数都假定关键字的全域为自然数集 N = { 0 , 1 , 2 , ⋯   } N=\{0, 1, 2, \cdots\} N={0,1,2,}。因此,如果所给关键字不是自然数,就需要找到一种方法来将它们转换为自然数。例如,一个字符串可以被转换为按适当的基数符号表示的整数。这样,就可以将标识符pt转换为十进制整数对(112,116),这是因为在ASCI字符集中,p=112t=116。然后,以128为基数来表示,pt即为 ( 112 × 128 ) + 116 = 14452 (112×128)+116=14452 (112×128)+116=14452

在一特定的应用场合,通常还能设计出其他类似的方法,将每个关键字转换为一个(可能是很大的)自然数。在后面的内容中,我们假定所给的关键字都是自然数。

除法散列法

在用来设计散列函数的除法散列法中,通过取 k k k除以 m m m的余数,将关键字 k k k映射到 m m m个槽中的某一个上,即散列函数为:
h ( k ) = k m o d    m h(k)= k\mod m h(k)=kmodm
例如,如果散列表的大小为 m = 12 m=12 m=12,所给关键字 k = 100 k=100 k=100,则 h ( k ) = 4 h(k)=4 h(k)=4。由于只需做一次除法操作,所以除法散列法是非常快的。

当应用除法散列法时,要避免选择 m m m的某些值。例如, m m m不应为2的幂,因为如果 m = 2 p m=2^p m=2p,则 h ( k ) h(k) h(k)就是 k k k p p p个最低位数字。除非已知各种最低 p p p位的排列形式为等可能的,否则在设计散列函数时,最好考虑关键字的所有位

一个不太接近2的整数幂的素数,常常是 m m m的一个较好的选择。例如,假定我们要分配张散列表并用链接法解决冲突,表中大约要存放 n = 2000 n=2000 n=2000个字符串,其中每个字符有8位。如果我们不介意一次不成功的查找需要平均检查3个元素,这样分配散列表的大小为 m = 701 m=701 m=701。选择701这个数的原因是,它是一个接近2000/3但又不接近2的任何次幂的素数。把每个关键字 k k k视为一个整数,则散列函数如下:
h ( k ) = k m o d    701 h(k)= k\mod 701 h(k)=kmod701

乘法散列法

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

  1. 用关键字 k k k乘上常数 A ( 0 < A < 1 ) A(0<A<1) A(0<A<1),并提取 k A kA kA的小数部分。
  2. m m m乘以这个值,再向下取整。

总之,散列函数为:
h ( k ) = ⌊ m ( k A m o d    1 ) ⌋ h(k)= \lfloor m(kA\mod1) \rfloor h(k)=m(kAmod1)⌋

这里 k A m o d    1 kA\mod1 kAmod1是取 k A kA kA的小数部分,即 k A − ⌊ k A ⌋ kA-\lfloor kA\rfloor kAkA

乘法散列法的一个优点是对 m m m的选择不是特别关键,一般选择它为2的某个幂次,即 m = 2 p m=2^p m=2p p p p为某个整数。这是因为我们可以在大多数计算机上,按下面所示方法较容易地实现散列函数。

假设某计算机的字长为 w w w位,而 k k k正好可用一个单字表示。限制 A A A为形如 s 2 w \frac{s}{2^w} 2ws的一个分数,其中 s s s是一个取自 0 < s < 2 w 0<s<2^w 0<s<2w的整数。参见下图,先用 w w w位整数 s = A ⋅ 2 w s=A·2^w s=A2w乘上 k k k,其结果是一个 2 w 2w 2w位的值 r 1 2 w + r 0 r_12^w+r_0 r12w+r0,这里 r 1 r_1 r1为乘积的高位字, r 0 r_0 r0为乘积的低位字。所求的 p p p位散列值中,包含了 r 0 r_0 r0 p p p个最高有效位。

虽然这个方法对任何的 A A A值都适用,但对某些值效果更好。最佳的选择与待散列的数据的特征有关,我们一般认为:
A = 5 − 1 2 ≈ 0.618033988 ⋯ A=\frac{\sqrt{5}-1}{2}\approx 0.618033988\cdots A=25 10.618033988
是一个比较理想的值。

全域散列法

如果让一个恶意的对手来针对某个特定的散列函数选择要散列的关键字,那么他会将 n n n个关键字全部散列到同一个槽中,使得平均的检索时间为 Θ ( n ) \Theta(n) Θ(n)。任何一个特定的散列函数都可能出现这种令人恐怖的最坏情况。唯一有效的改进方法是随机地选择散列函数,使之独立于要存储的关键字。这种方法称为全域散列,不管对手选择了怎么样的关键字,其平均性能都很好。全域散列法在执行开始时,就从一组精心设计的函数中,随机地选择一个作为散列函数。就像在快速排序中一样,随机化保证了没有哪一种输入会始终导致最坏情况性能。因为随机地选择散列函数,算法在每一次执行时都会有所不同,甚至对于相同的输入都会如此。这样就可以确保对于任何输入,算法都具有较好的平均情况性能。

再回到编译器的符号表的例子,在全域散列方法中,可以发现程序员对标识符的选择就不会总是导致较差的散列性能了。仅当编译器选择了一个随机的散列函数,使得标识符的散列效果较差时,才会出现较差的性能。但出现这种情况的概率很小,并且这一概率对任何相同大小的标识符集来说都是一样的。

H \mathcal{H} H为一组有限散列函数,它将给定的关键字全域 U U U映射到 { 0 , 1 , ⋯   , m − 1 } \{0, 1, \cdots, m-1\} {0,1,,m1}中。这样的个函数组称为全域的,如果对每一对不同的关键字 k , l ∈ U k,l\in U k,lU,满足 h ( k ) = h ( l ) h(k)=h(l) h(k)=h(l)的散列函数h∈的个数至多为 H m \frac{\mathcal{H}}{m} mH。换句话说,如果从 H \mathcal{H} H中随机地选择一个散列函数,当关键字 k ≠ l k≠l k=l时,两者发生冲突的概率不大于 1 m \frac{1}{m} m1,这也正好是从集合 { 0 , 1 , ⋯   , m − 1 } \{0, 1, \cdots, m-1\} {0,1,,m1}中独立地随机选择 h ( k ) h(k) h(k) h ( l ) h(l) h(l)时发生冲突的概率。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值