Java hashmap真的是O(1)吗?

时间:2009-06-28 16:49:26

标签: java hashmap big-o time-complexity

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

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

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

15 个答案:

答案 0 :(得分:115)

HashMap的一个特殊功能是,与平衡树不同,它的行为是概率性的。在这些情况下,通常最有助于谈论最坏情况事件发生概率的复杂性。对于哈希映射,当然是关于地图恰好是多么充分的碰撞的情况。碰撞很容易估计。

  

p collision = n / capacity

因此,即使是适度数量的元素的哈希映射也很可能会遇到至少一次冲突。 Big O表示法允许我们做一些更引人注目的事情。观察任何任意固定常数k。

  

O(n)= O(k * n)

我们可以使用此功能来改善哈希映射的性能。我们可以考虑最多2次碰撞的概率。

  

p collision x 2 =(n / capacity) 2

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

  

p collision x k =(n / capacity) k

现在我们可以忽略一些任意数量的碰撞,最终导致碰撞的可能性微乎其微。通过选择正确的k,您可以将概率提高到任意微小的水平,所有这些都不会改变算法的实际实现。

我们通过说哈希图具有O(1)访问 的概率很高

来讨论这个问题

答案 1 :(得分:36)

您似乎将最坏情况行为与平均情况(预期)运行时混淆。前者实际上是哈希表的O(n)(即不使用完美的哈希),但这在实践中很少有用。

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

答案 2 :(得分:29)

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

因此,有时它必须与几个项目进行比较,但通常它比O(n)更接近O(1)。出于实用目的,您应该知道这一切。

答案 3 :(得分:27)

请记住,o(1)并不意味着每个查询只检查一个项目 - 这意味着检查的项目的平均数量保持不变w.r.t.容器中的项目数。因此,如果平均需要4次比较来查找具有100个项目的容器中的项目,则还应该平均进行4次比较以在具有10000个项目的容器中找到项目,并且对于任何其他数量的项目(总是存在方差,特别是在哈希表重新出现的点附近,以及当项目数量非常少时)。

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

答案 4 :(得分:11)

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

严格来说,正确的是哈希映射并不是O(1),因为随着元素数量的增加,最终你将无法在恒定时间内搜索(和O符号)是根据可以任意大的数字来定义的。

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

事实上,Java 8一旦超过阈值就会将桶实现为TreeMaps,这使得实际时间为O(log n)

答案 5 :(得分:4)

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

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

答案 6 :(得分:4)

O(1+n/k)其中k是存储桶的数量。

如果实施设置为k = n/alpha,那么它是O(1+alpha) = O(1),因为alpha是常量。

答案 7 :(得分:2)

我们已经确定哈希表查找的标准描述为O(1)是指平均情况预期时间,而不是严格的最坏情况性能。对于解析与链接冲突的哈希表(如Java的hashmap),这在技术上是O(1 +α)和a good hash function,其中α是表的加载因子。只要您存储的对象数量不超过大于表格大小的常数因子,它仍然是常量。

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

如果您对实现恒定时间预期的最坏情况查找的理论方法感兴趣,您可以阅读dynamic perfect hashing,它以另一个哈希表递归地解决冲突!

答案 8 :(得分:2)

只有当您的散列函数非常好时才是O(1)。 Java哈希表实现不能防止错误的哈希函数。

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

答案 9 :(得分:2)

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

location = (arraylength - 1) & keyhashcode

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

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

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

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

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

答案 10 :(得分:1)

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

如果表中没有冲突,您只需要进行一次查找,因此运行时间为O(1)。如果存在冲突,则必须进行多次查找,从而将性能降低到O(n)。

答案 11 :(得分:1)

这取决于您选择的算法以避免碰撞。如果您的实现使用单独的链接,则最糟糕的情况发生在每个数据元素被散列为相同的值(例如,散列函数选择不当)的情况下。在那种情况下,数据查找与链表上的线性搜索即O(n)没有区别。但是,发生这种情况的概率可以忽略不计,查找效果最好,平均情况保持不变,即O(1)。

答案 12 :(得分:1)

除了学术之外,从实际角度来看,HashMaps应被视为具有无关紧要的性能影响(除非您的分析器另有说明。)

答案 13 :(得分:1)

仅在理论情况下,当哈希码总是不同并且每个哈希码的桶也不同时,O(1)将存在。否则,它是恒定的顺序,即在散列映射的增量上,它的搜索顺序保持不变。

答案 14 :(得分:0)

当然,hashmap的性能将取决于给定对象的hashCode()函数的质量。但是,如果实现该功能使得冲突的可能性非常低,那么它将具有非常好的性能(在每个可能的情况下,这不是严格的O(1),而是在大多数案例。)

例如,Oracle JRE中的默认实现是使用随机数(存储在对象实例中以便它不会更改 - 但它也会禁用偏向锁定,但这是另一个讨论)所以机会碰撞非常低。