HashMap得到/放置复杂性

时间:2010-12-29 11:22:03

标签: java data-structures hashmap complexity-theory

我们习惯说HashMap get/put操作是O(1)。但是它取决于哈希实现。默认对象哈希实际上是JVM堆中的内部地址。我们是否确定声称get/put是O(1)?

是否足够好

可用内存是另一个问题。据我所知,javadocs HashMap load factor应为0.75。如果我们在JVM中没有足够的内存并且load factor超出限制怎么办?

所以,看起来O(1)似乎不能保证。它有意义还是我错过了什么?

6 个答案:

答案 0 :(得分:188)

这取决于很多事情。它通常是 O(1),有一个不错的哈希,它本身就是恒定的时间......但你可能有一个需要很长时间才能计算的哈希值,如果有的话哈希映射中的多个项目返回相同的哈希码,get必须迭代它们,在每个项目上调用equals以找到匹配项。

在最坏的情况下,HashMap由于遍历同一散列桶中的所有条目而具有O(n)查找(例如,如果它们都具有相同的散列码)。幸运的是,根据我的经验,这种最糟糕的情况在现实生活中并不经常出现。所以不,O(1)当然不能保证 - 但通常在考虑使用哪种算法和数据结构时应该假设。

在JDK 8中,HashMap已被调整,以便如果可以比较密钥以进行排序,那么任何密集填充的存储桶都将实现为树,因此即使存在大量具有相同哈希的条目代码,复杂度为O(log n)。如果您的密钥类型的相等和排序不同,那么这可能会导致问题。

是的,如果你没有足够的内存用于哈希映射,你就会遇到麻烦......但无论你使用什么数据结构,这都是正确的。

答案 1 :(得分:9)

我不确定默认的哈希码是地址 - 我前一段时间读取了OpenJDK源的哈希码生成,我记得它有点复杂。也许并不是保证良好分配的东西。但是,这在某种程度上没有实际意义,因为您在hashmap中用作键的类很少使用默认的哈希码 - 它们提供了自己的实现,这应该是好的。

最重要的是,你可能不知道的(再次,这是基于阅读源 - 它不能保证)是HashMap在使用之前激活哈希,将整个单词的熵混合到底部位,除了最大的哈希映射之外的所有地方都需要它。这有助于处理那些本身并没有这样做的哈希,虽然我无法想到你会看到的任何常见情况。

最后,当表被重载时会发生的事情是它退化为一组并行链表 - 性能变为O(n)。具体来说,遍历的链接数量平均是负载系数的一半。

答案 2 :(得分:8)

已经提到,如果O(n/m)是项目数且n是大小,则散列图平均为m。还有人提到,原则上整个事情可能会崩溃成一个O(n)查询时间的单链表。 (这都假设计算哈希是恒定时间)。

然而,经常提到的是,概率至少为1-1/n(因此对于1000个项目的概率为99.9%),最大存储桶的填充量不会超过O(logn)!因此匹配二叉搜索树的平均复杂度。 (而且常数很好,更严格的界限是(log n)*(m/n) + O(1))。

这个理论界限所需要的是你使用一个相当好的哈希函数(参见维基百科:Universal Hashing。它可以像a*x>>m一样简单)。当然,给你哈希值的人不知道你是如何选择随机常数的。

TL; DR:具有非常高的概率,最糟糕的情况是获取/放置散列图的复杂性为O(logn)

答案 3 :(得分:7)

HashMap操作是hashCode实现的依赖因素。对于理想情况,可以说为每个对象提供唯一哈希码的良好哈希实现(无哈希冲突),那么最佳,最差和平均情况将是O(1)。 让我们考虑一个场景,其中hashCode的错误实现总是返回1或具有哈希冲突的此类哈希。在这种情况下,时间复杂度为O(n)。

现在谈到关于内存的问题的第二部分,那么JVM会照顾内存约束。

答案 4 :(得分:3)

我同意:

  • O(1)的一般摊销复杂度
  • 错误的hashCode()实现可能会导致多次冲突,这意味着在最坏的情况下,每个对象都将进入同一存储桶,因此,如果每个存储桶都由以下支持,则O( N List
  • 因为Java 8 HashMap动态地将每个存储桶中使用的Nodes(链接列表)替换为TreeNodes(当列表大于8个元素时为红黑树),从而导致O( logN )。

但是,如果我们想做到100%精确,这并不是全部。 hashCode()的实现,键Object的类型(不可变/已缓存或为Collection)也可能严格地影响实际的复杂性。

我们假设以下三种情况:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

它们是否具有相同的复杂性?好吧,正如预期的那样,第一个的摊销复杂度为O(1)。但是,对于其余部分,我们还需要计算lookup元素的hashCode(),这意味着我们可能必须遍历算法中的数组和列表。

让我们假设以上所有数组/列表的大小为 k 。 然后,HashMap<String, V>HashMap<List<E>, V>将具有O(k)摊销的复杂度,并且类似地,在Java8中,O( k + logN )最坏的情况。

*请注意,使用String键是更复杂的情况,因为它是不可变的,并且Java将hashCode()的结果缓存在私有变量hash中,因此只计算一次

/** Cache the hash code for the string */
    private int hash; // Default to 0

但是,以上情况也有其自身的最坏情况,因为Java的String.hashCode()实现正在计算hash == 0之前检查是否hashCode。但是,嘿,有一些非空字符串输出的hashcode为零,例如“ f5a5a608”,请参见here,在这种情况下,备注可能无济于事。

答案 5 :(得分:2)

在实践中,它是O(1),但这实际上是一种可怕的,数学上无意义的简化。 O()表示法说明当问题的大小趋于无穷大时算法的行为。 Hashmap get / put的工作方式类似于有限大小的O(1)算法。从计算机内存和寻址的角度来看,这个限制相当大,但远非无穷大。

当有人说hashmap get / put是O(1)时,它应该说get / put所需的时间或多或少是不变的,并且不依赖于hashmap中的元素数量。 hashmap可以在实际的计算系统上呈现。如果问题超出了那个大小并且我们需要更大的哈希映射,那么一段时间之后,当我们用完可能可描述的不同元素时,当然描述一个元素的位数也会增加。例如,如果我们使用散列映射存储32位数字,稍后我们增加问题大小,以便我们在散列映射中有超过2 ^ 32位元素,那么将使用超过32位来描述各个元素。

描述各个元素所需的位数是log(N),其中N是元素的最大数量,因此get和put实际上是O(log N)。

如果将它与树集(即O(log n))进行比较,则哈希集为O(long(max(n)),我们只是觉得这是O​​(1),因为在某个实现上最大(n)是固定的,不会改变(我们以比特为单位测量的对象的大小),并且计算哈希码的算法很快。

最后,如果在任何数据结构中找到一个元素是O(1),我们就会凭空创造信息。具有n个元素的数据结构I可以以n个不同的方式选择一个元素。有了它,我可以编码log(n)位信息。如果我可以用零位编码(这就是O(1)的意思)那么我创建了一个无限压缩的ZIP算法。