为什么Knuth、CLRS散列函数不够



很容易证明,给定一组分布未知的键,我们不能构造一个函数,以这些键为输入,输出均匀分布的值。

因此,我们研究用于未知分布的通用散列函数。

Knuth建议利用无理数的非重复数字,尤其是黄金比例,以便在表范围内均匀分布键。

CLRS建议只需将键mod取一个大素数,就可以在表范围内大致均匀地分配键,并分解重复模式。

在这两种情况下,目标似乎都是平均分配密钥。

但是,当考虑Murmur2、SeaHash等解决方案时,他们似乎花了很多精力来确保"蝴蝶式"效果:给定一个密钥,更改任何1位都有很好的机会更改哈希中的每一位。

为什么这种行为是可取的?TAOCP和CLRS中提出的这些解决方案的缺点是什么?

如果所需的行为是分解输入键集中的任何模式,那么这里隐含的假设是,显示任何类型模式的键集更有可能是野生的?这合理吗?

如果我说得不准确,我很抱歉。

编辑:不需要具有加密强度。目的是尽量减少碰撞。

我不能100%确定这一点,但这可能是不同作者在不同背景下做出的不同假设的产物。

Knuth在TAoCP中的工作是在开发任何其他书籍或散列函数之前完成的。当时,Knuth在某种意义上是如何分析和思考不同算法和数据结构的先驱。当时,"用某种方案把东西分到桶里"的想法是众所周知的,但没有人认真思考如何最好地选择这种方案。他的方法在数学上简单而优雅,并在当代(20世纪70年代)硬件上快速运行。总的主题是"如果你要根据某个函数进行分配,这里有一个非常好且简单的函数,它背后有一个很好的理论。">

Knuth分析数据结构或算法的第一篇论文IIRC是关于哈希表的。他在假设哈希码是一致随机的基础上进行了分析,并表明在这些假设下,哈希表表现良好。

不过,正如您所提到的,很明显,如果您选择任何固定的哈希函数,您将有退化的输入情况。很多人开始思考如何处理这个问题,许多人尝试从可用的哈希函数池中随机选择一个哈希函数。20世纪70年代末,Wegman发表了一篇题为《哈希函数的通用类》的论文,该论文概述了一个正式的数学定义,即哈希函数族是一个很好的哈希函数类。本文证明了通用散列函数族具有较低的预期碰撞次数,这使得它们非常适合链式散列表。

CLRS的第一版于1990年出版,结合了Knuth对线性探测的分析(假设真正的随机哈希码)和使用通用哈希的链式哈希表的分析。换句话说,它承认在选择哈希函数时必须小心(没有一个固定函数总是有效的,所以看看通用哈希),但也会在假设你有一个"足够好"的哈希函数的情况下进行一些计算。

(后来的理论发展包括里程碑式的论文《为什么简单哈希函数有效》,解释了为什么弱哈希函数与输入分布中的少量熵相结合,本质上就像真正的随机函数,以及后来的一些工作表明,5个独立的哈希函数是在线性探测表中获得良好性能所需的全部。)

上述所有工作都在Theoryland进行,目标是建立良好的数学框架来分析数据结构,并为实践中获得良好分布和效率的方法提出具体建议。

然后是《真实世界》,从业者并不总是掌握数学,数学往往落后于从业者所做的。

如果您查看大多数关于哈希函数的工作,许多哈希函数都认为您使用的数据可以很容易地以有意义的方式分解为整数单位。但实际数据并不总是以这种方式很好地分解。或者,你可能有一种像C++、Java、Python等语言,其中每个对象都有"一个"哈希代码,即与之相关的哈希代码,而不是人们建议的具有可用哈希函数族的理论。

在这种情况下,尝试构建一个哈希函数并不是没有道理的,它(1)评估速度快得离谱,(2)可以在同一程序或多台机器的不同运行中工作,(3)在实践中工作得"足够好",人们不会抱怨。这就是你得到MurmurHash之类的散列函数的地方——它们非常非常好地满足了这一需求。假设您没有针对对抗性选择的输入进行操作,那么这类散列函数就可以了。

有趣的是,我们现在看到Knuth乘法散列函数。可以将不同的散列函数组合在一起的库,如Boost的hash_combine,使用该技术可以在给定多个现有散列作为输入的情况下提供确定性良好的散列代码。

总结:

  • 这些差异很多都是历史性的。Knuth为如何分析散列函数奠定了理论基础,并考虑了只有一个散列函数的情况。后来对通用散列的研究为处理散列函数类提供了不同的视角和框架。

  • 理论和实践之间总是有差距的。对于非对抗性的情况,像MurmurHash这样的非随机散列简单、快速且工作良好。与单个整数值相比,它们也能很好地处理可变长度的输入。

最新更新