HashMap 获取/放置复杂度

Posted

技术标签:

【中文标题】HashMap 获取/放置复杂度【英文标题】:HashMap get/put complexity 【发布时间】:2011-06-01 00:24:50 【问题描述】:

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

可用内存是另一个问题。正如我从 javadocs 中了解到的,HashMap 负载因子应该是 0.75。如果 JVM 内存不足,负载因子超过限制怎么办?

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

【问题讨论】:

您可能想查找摊销复杂性的概念。例如,请参见此处:***.com/questions/3949217/time-complexity-of-hash-table 最坏情况复杂性不是哈希表最重要的衡量标准 正确 -- 摊销 O(1) -- 永远不要忘记第一部分,你不会有这类问题 :) 如果我没记错的话,自 Java 1.8 以来最坏的时间复杂度是 O(logN)。 【参考方案1】:

这取决于很多事情。 通常 O(1),有一个像样的散列,它本身是常数时间......但是你可能有一个需要很长时间来计算的散列,并且如果有是哈希映射中返回相同哈希码的多个项目,get 必须遍历它们,对它们中的每一个调用 equals 以找到匹配项。

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

在 JDK 8 中,HashMap 进行了调整,以便如果可以比较键以进行排序,那么任何密集填充的存储桶都将实现为树,这样即使有很多具有相同哈希码的条目,复杂度为 O(log n)。当然,如果您有一个相等和顺序不同的键类型,这可能会导致问题。

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

【讨论】:

@marcog:您假设单次查找的 O(n log n)?这对我来说听起来很愚蠢。当然,这取决于哈希和相等函数的复杂性,但这不太可能取决于地图的大小。 @marcog:那么你假设 O(n log n) 是多少?插入 n 个项目? +1 以获得好的答案。您能否在答案中提供this wikipedia entry for hash table 之类的链接?这样,更感兴趣的读者可以深入了解为什么您给出了答案。 @SleimanJneidi:如果密钥没有实现 Comparable`,它仍然是 - 但我会在有更多时间时更新答案。 @ip696:是的,put 是“摊销 O(1)”——通常为 O(1),偶尔为 O(n)——但很少足以平衡。【参考方案2】:

如果n 是项目数,m 是大小,则已经提到哈希图平均为O(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:在极高概率下,hashmap 的最坏情况 get/put 复杂度为 O(logn)

【讨论】:

(请注意,这些都不是假设随机数据。概率纯粹来自哈希函数的选择) 我也有关于哈希映射中查找的运行时复杂性的相同问题。看起来它是 O(n),因为应该删除常数因子。 1/m 是一个常数因子,因此被丢弃,留下 O(n)。 人们需要了解什么是 Big Theta,并在他们想说“平均 Big-O”时使用它,因为 Big-O 是最坏的情况。 @nickdu 1/m 肯定不是一个不变的因素。您需要像n(项目数)一样快地增长m(桶数)。您不会从具有常量 m 的哈希映射中获得太多好处。【参考方案3】:

我不确定默认的哈希码是地址 - 我不久前阅读了用于生成哈希码的 OpenJDK 源代码,我记得它有点复杂。也许仍然不能保证良好的分布。然而,这在某种程度上是没有意义的,因为您在 hashmap 中用作键的类很少使用默认的 hashcode - 它们提供自己的实现,这应该是好的。

最重要的是,您可能不知道(再次,这是基于阅读源 - 它不能保证)是 HashMap 在使用它之前搅拌散列,将整个单词的熵混合到底部位中,这是除了最大的哈希图之外的所有地方都需要它的地方。这有助于处理专门不这样做的哈希,尽管我想不出你会看到的任何常见情况。

最后,当表超载时,会退化为一组并行链表——性能变为 O(n)。具体来说,遍历的链接数平均为负载因子的一半。

【讨论】:

该死的。我选择相信,如果我不必在翻转的手机触摸屏上打字,我本可以击败 Jon Sheet。有一个徽章,对吧?【参考方案4】:

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

现在来到关于内存的问题的第二部分,那么是的,JVM 会处理内存限制。

【讨论】:

【参考方案5】:

我同意:

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

但是,如果我们想要 100% 准确,这不是完整的事实。 hashCode() 的实现和键 Object 的类型(不可变/缓存或作为集合)也可能会影响严格意义上的实时复杂度。

让我们假设以下三种情况:

    HashMap<Integer, V> HashMap<String, V> HashMap<List<E>, V>

它们是否具有相同的复杂性?好吧,正如预期的那样,第一个的摊销复杂度是 O(1)。但是,对于其余部分,我们还需要计算查找元素的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() 实现在计算 hashCode 之前检查是否 hash == 0。但是,嘿,有非空字符串输出为零的hashcode,例如“f5a5a608”,请参阅here,在这种情况下,记忆可能没有帮助。

【讨论】:

【参考方案6】:

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

当人们说 hashmap get/put 是 O(1) 时,实际上应该说 get/put 所需的时间或多或少是恒定的,并且不依赖于 hashmap 中的元素数量,因为hashmap可以呈现在实际的计算系统上。如果问题超出了这个大小并且我们需要更大的哈希图,那么一段时间后,描述一个元素的位数肯定也会随着我们用完可能的可描述的不同元素而增加。例如,如果我们使用 hashmap 来存储 32bit 的数字,然后我们增加问题大小,使得 hashmap 中的元素超过 2^32bit,那么单个元素将被描述为超过 32bit。

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

如果你将它与一个树集进行比较,它是 O(log n),那么哈希集是 O(long(max(n)),我们只是觉得这是 O(1),因为在某个实现中 max (n) 是固定的,不会改变(我们存储的对象的大小以位为单位)并且计算哈希码的算法很快。

最后,如果在任何数据结构中找到一个元素的时间为 O(1),我们就会凭空创建信息。拥有 n 个元素的数据结构,我可以以 n 种不同的方式选择一个元素。有了它,我可以对 log(n) 位信息进行编码。如果我可以将其编码为零位(这就是 O(1) 的含义),那么我创建了一个无限压缩 ZIP 算法。

【讨论】:

不应该是树集O(log(n) * log(max(n)))的复杂度,那么?虽然每个节点的比较可能更智能,但在最坏的情况下,它需要检查所有O(log(max(n)) 位,对吧?

以上是关于HashMap 获取/放置复杂度的主要内容,如果未能解决你的问题,请参考以下文章

HashMap 获取/放置复杂度

从集合中获取随机元素

面试题:HashSetTreeSet 和HashMap 的实现与原理

Java集合系列-HashMap 1.8

为啥HashMap的运算复杂度没有考虑hash函数的复杂度? [复制]

阿里巴巴JAVA面试真题