Java哈希图真的是O(1)吗?


159

我已经看到了一些关于Java哈希图及其O(1)查找时间的有趣声明。有人可以解释为什么会这样吗?除非这些哈希图与我购买的任何哈希算法有很大不同,否则必须始终存在包含冲突的数据集。

在这种情况下,查找将O(n)不是O(1)

有人可以解释他们是否 O(1),如果是,他们如何实现这一目标?


1
我知道这可能不是答案,但是我记得Wikipedia上有一篇很好的文章。不要错过性能分析部分
Victor Hugo

28
大O标记为您正在执行的特定分析类型提供了上限。你应该还指定是你有兴趣在最坏的情况下,平均情况,等等
丹Homerick

Answers:


127

HashMap的一个特殊功能是与平衡树不同,它的行为是概率性的。在这些情况下,就最坏情况发生的可能性而言,谈论复杂性通常是最有帮助的。对于散列图,当然是关于图恰好满了的情况。碰撞非常容易估计。

p 碰撞 = n /容量

因此,即使元素数量很少的哈希图也很可能会经历至少一次碰撞。大O表示法使我们可以做一些更具吸引力的事情。观察到任意任意的固定常数k。

O(n)= O(k * n)

我们可以使用此功能来改善哈希图的性能。我们可以考虑最多发生两次碰撞的可能性。

p 碰撞x 2 =(n /容量)2

这要低得多。由于处理一次额外碰撞的成本与Big O性能无关,因此我们找到了一种无需实际更改算法即可提高性能的方法!我们可以将其概括为

p 碰撞xk =(n /容量)k

现在,我们可以忽略任意数量的碰撞,并且最终产生的碰撞比我们所考虑的要少得多。通过选择正确的k,您可以将概率降低到任意微小的水平,而无需改变算法的实际实现。

我们通过说哈希映射很有可能具有O(1)访问谈论此问题


即使使用HTML,我仍然对分数并不满意。如果可以想到一种好的方法,请清理它们。
SingleNegationElimination

4
实际上,以上所说的是,对于N的非极值,O(log N)效应被固定的开销所掩盖。
2014年

从技术上讲,您给出的数字是碰撞次数的期望值,可以等于一次碰撞的概率。
Simon Kuang 2015年

1
这类似于摊销分析吗?
lostsoul29年

1
@ OleV.V。HashMap的良好性能始终取决于散列函数的良好分布。您可以通过在输入上使用加密散列函数来以更好的散列质量换取散列速度。
SingleNegationElimination

38

您似乎将最坏情况的行为与平均情况(预期)的运行时混合在一起。对于散列表,前者的确确实是O(n)(即,不使用完美的散列),但实际上很少相关。

任何可靠的哈希表实现,加上半个体面的哈希,在预期的情况下,在非常窄的方差范围内,具有非常小的因数(实际上为2)的O(1)检索性能。


6
我一直认为上限是最坏的情况,但似乎我弄错了-您可以为一般情况设置上限。因此看来,声称O(1)的人应该明确表示这是一般情况。最坏的情况是一个数据集,其中有许多冲突使它成为O(n)。现在这很有意义。
paxdiablo

2
您可能应该清楚地表明,对于一般情况使用大O表示法时,您正在谈论的是预期运行时函数的上限,该函数是一个明确定义的数学函数。否则,您的答案就没有多大意义了。
ldog

1
gmatt:我不确定我是否理解您的反对意见:big-O表示法是函数定义的上限。因此,我还有什么意思?
康拉德·鲁道夫

3
通常在计算机文献中,您会看到大的O表示法表示算法的运行时或空间复杂度函数的上限。在这种情况下,上限实际上是期望值,它本身不是函数,而是函数的操作符(随机变量),实际上是整数(lebesgue)。可以绑定这样的东西这一事实不应该被接受。是理所当然的,也不是小事。
ldog

31

在Java中,HashMap通过使用hashCode来定位存储桶。每个存储桶是该存储桶中的项目列表。扫描项目,使用等于进行比较。添加项目时,一旦达到一定的加载百分比,将调整HashMap的大小。

因此,有时必须将其与几个项目进行比较,但通常它比O(n)更接近O(1)。出于实际目的,这就是您应该知道的所有内容。


11
好吧,由于应该使用big-O来指定限制,因此是否接近O(1)并没有什么区别。即使O(n / 10 ^ 100)仍然是O(n)。我的意思是效率降低了比率,但是仍然使算法为O(n)。
paxdiablo 2009年

4
哈希映射分析通常是在平均情况下进行的,即O(1)(有共谋)。在最坏的情况下,您可以拥有O(n),但通常情况并非如此。关于差异-O(1)意味着无论图表上的项目数量如何,您都将获得相同的访问时间,并且通常是这种情况(只要表的大小和'n ')
Liran Orevi 2009年

4
还值得注意的是,即使对存储桶的扫描花费了一段时间,因为它中已经包含一些元素,它仍然是O(1)。只要铲斗具有固定的最大尺寸,这就是与O()分类无关的常数因子。但是,当然可以添加更多带有“相似”键的元素,以便这些存储桶溢出,并且您不能再保证常量。
某事

@sth为什么水桶有固定的最大尺寸!?
纳文

31

请记住,o(1)并不意味着每个查找都只检查一个项目-这意味着检查的平均项目数与容器中的项目数保持不变。因此,如果平均需要进行4次比较才能在包含100个项目的容器中找到一个项目,那么还应该平均进行4次比较才能在具有10000个项目的容器中找到一个项目,对于任何其他数量的项目(总是存在一个差异,尤其是在哈希表经过哈希的点以及项目数量很少时)。

因此,只要每个存储桶的平均键数保持在固定范围内,冲突就不会阻止容器执行o(1)操作。


16

我知道这是一个老问题,但实际上有一个新答案。

没错O(1),严格来说,哈希映射并不是真的,因为随着元素数量的任意增加,最终您将无法在恒定时间内进行搜索(并且O标记是根据可以任意增大)。

但这并不能说明实时复杂性是O(n)-因为没有规则说必须将存储桶实现为线性列表。

实际上,Java 8在存储桶TreeMaps超过阈值时就实现了存储桶,这使实际时间变为O(log n)


4

如果存储桶的数量(称为b)保持恒定(通常情况),则查找实际上为O(n)。
当n变大时,每个存储桶中的元素数平均为n / b。如果以常用方式之一(例如,链表)完成冲突解决,则查找为O(n / b)= O(n)。

O符号表示当n越来越大时会发生什么。当将其应用于某些算法时,可能会产生误导,哈希表就是一个很好的例子。我们根据期望处理的元素数选择存储桶数。当n的大小与b大致相同时,则查找大致是恒定时间,但我们不能称其为O(1),因为O的定义是将其限制为n→∞。



2

我们已经建立了哈希表查找的标准描述为O(1)是指平均情况下的预期时间,而不是严格的最坏情况下的性能。对于通过链表解决冲突的哈希表(例如Java的哈希图),从技术上讲,它是O(1 +α),具有良好的哈希函数,其中α是表的负载因子。只要您存储的对象数不超过表大小的常数,该常数就保持不变。

也有人解释说,严格来说,可以为任何确定性哈希函数构造需要O(n)查找的输入。但是考虑最坏情况下的预期时间也很有趣,该时间不同于平均搜索时间。使用链时,这是O(1 +最长链的长度),例如,当α= 1时为Θ(log n / log log n)。

如果您对实现恒定时间预期的最坏情况查找的理论方法感兴趣,则可以阅读有关动态完美哈希的知识,该方法可以用递归方式解决与另一个哈希表的冲突!


2

仅当您的哈希函数非常好时才为O(1)。Java哈希表实现无法防止不良的哈希函数。

添加项目时是否需要增加表格与该问题无关,因为它与查找时间有关。


2

HashMap内的元素存储为一个链表(节点)数组,该数组中的每个链表代表一个或多个键的唯一哈希值的存储桶。
在HashMap中添加条目时,键的哈希码用于确定存储桶在数组中的位置,例如:

location = (arraylength - 1) & keyhashcode

这里的&表示按位AND运算符。

例如: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

在获取操作期间,它使用相同的方法来确定钥匙的存储桶的位置。在最佳情况下,每个键都有唯一的哈希码,并为每个键生成唯一的存储桶,在这种情况下,get方法仅花费时间来确定存储桶的位置并检索常量O(1)的值。

在最坏的情况下,所有键都具有相同的哈希码并存储在相同的存储桶中,这导致遍历整个列表,从而导致O(n)。

在Java 8的情况下,如果大小增加到大于8,则将链表列表桶替换为TreeMap,这会将最坏情况的搜索效率降低到O(log n)。


1

这基本上适用于大多数编程语言中的大多数哈希表实现,因为算法本身并没有真正改变。

如果表中没有冲突,则只需进行一次查找,因此运行时间为O(1)。如果存在冲突,则必须进行多个查找,这会降低性能,趋向O(n)。


1
假设运行时间受查找时间限制。在实践中,您会发现哈希函数提供边界(字符串)的很多情况
Stephan Eggermont 2009年

1

这取决于您选择避免冲突的算法。如果您的实现使用单独的链接,则最坏的情况是每个数据元素都被散列为相同的值(例如,散列函数选择不当)。在这种情况下,数据查找与对链表(即O(n))的线性搜索没有什么不同。但是,发生这种情况的可能性可以忽略不计,并且最佳查找和平均情况保持不变,即O(1)。


1

除了学者,​​从实践的角度看,HashMaps应该被认为具有不重要的性能影响(除非您的分析器告诉您其他情况)。


4
不在实际应用中。一旦使用字符串作为键,您会发现并非所有哈希函数都是理想的,有些哈希函数确实很慢。
2009年

1

仅在理论上,当哈希码始终不同并且每个哈希码的存储桶也不同时,O(1)将存在。否则,它具有恒定的顺序,即在哈希图递增时,其搜索顺序保持恒定。


0

当然,哈希图的性能将取决于给定对象的hashCode()函数的质量。但是,如果实现该函数使得发生碰撞的可能性非常低,则它将具有很好的性能(在每种可能的情况下,严格来说,这不是严格的O(1),但在大多数情况下,情况)。

例如,Oracle JRE中的默认实现是使用随机数(该随机数存储在对象实例中,因此它不会改变-但它也禁用了偏向锁定,但这是另一个讨论),因此发生冲突的可能性是非常低。


“在大多数情况下”。更具体地说,随着N趋于无穷大,总时间将趋于K乘以N(其中K为常数)。
ChrisW

7
错了 哈希表中的索引将被确定,hashCode % tableSize这意味着肯定存在冲突。您没有充分利用32位。这就是哈希表的意义所在……您可以将较大的索引空间减小为较小的索引空间。
2009年

1
“保证不会发生冲突”否,不是因为映射的大小小于哈希的大小:例如,如果映射的大小为2,则可以保证发生冲突(无关紧要什么哈希)如果/当我尝试插入三个元素。
ChrisW

但是,如何从密钥转换为O(1)中的内存地址?我的意思是像x = array [“ key”]。关键不是内存地址,因此它仍然必须是O(n)查找。
paxdiablo

1
“我相信,如果不实现hashCode,它将使用对象的内存地址”。它可以使用它,但是标准Oracle Java的默认hashCode实际上是存储在对象标头中的25位随机数,因此64/32位无关紧要。
Boann 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.