HashMap的获取/输入复杂度


131

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

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

因此,似乎无法保证O(1)。是有意义还是我想念什么?


1
您可能需要查找摊余复杂性的概念。例如,在此处查看:stackoverflow.com/questions/3949217/time-complexity-hash-table最坏情况下的复杂度不是哈希表的最重要衡量指标
G博士,2010年

3
正确- 分摊 O(1)-永远不要忘记第一部分,您将不会遇到这类问题:)
工程师

如果我没看错,那么从Java 1.8开始,时间复杂度最差的情况就是O(logN)。
塔伦·科拉

Answers:


216

这取决于很多事情。这通常是 O(1),一个体面的哈希它本身是固定的时间...但你可以有一个哈希这需要很长的时间来计算,如果在散列图多个项目,其中返回相同的散列码,get将不得不遍历他们,呼吁他们equals每个人寻找比赛。

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

在JDK 8中,HashMap已经进行了调整,以便可以比较键的排序方式,然后将任何人口稠密的存储桶都实现为树,因此即使有很多具有相同哈希码的条目,复杂度也为O(log n)。当然,如果您的键类型的相等性和顺序不同,则可能会导致问题。

是的,如果您没有足够的内存来存储哈希映射,则会遇到麻烦……但是,无论使用哪种数据结构,这都是正确的。


@marcog:您假设一次查找为O(n log n)吗?对我来说这听起来很愚蠢。当然,这将取决于哈希和相等函数的复杂性,但这不太可能取决于映射的大小。
乔恩·斯基特

1
@marcog:那你假设是O(n log n)?插入n个项目?
乔恩·斯基特

1
+1是个好答案。您能否在答案中提供像哈希表这样的Wikipedia条目链接?这样,更感兴趣的读者就可以理解为什么给出答案了。
David Weiser 2010年

2
@SleimanJneidi:仍然是如果键没有实现Comparable <T>`,但是我有更多时间时会更新答案。
乔恩·斯基特

1
@ ip696:是的,put是“摊销O(1)”-通常是O(1),偶尔是O(n)-但很少能平衡。
乔恩·斯基特

9

我不确定默认的哈希码是否为地址-前一阵子我读过OpenJDK源代码来生成哈希码,但我记得它有点复杂。也许仍然不能保证良好的发行。但是,从某种程度上讲,没有什么用,因为在哈希图中用作键的类很少使用默认的哈希码-它们提供了自己的实现,应该很好。

最重要的是,您可能不知道(同样,这是基于阅读源的-不能保证)是HashMap在使用哈希之前先对其进行搅拌,将整个单词的熵混合到最低位,也就是最低位除了最大的哈希图以外的所有图都需要。尽管我无法想到任何常见的情况,但这样做有助于处理那些本身并不专门执行的哈希。

最后,当表超载时发生的事情是,它退化为一组并行链接列表-性能变为O(n)。具体而言,所遍历的链路数平均将为负载因子的一半。


6
该死 我选择相信,如果我不必在翻转的手机触摸屏上键入此字符,那我本该击败Jon Sheet。有一个徽章,对不对?
汤姆·安德森

8

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

现在进入有关内存问题的第二部分,然后是,JVM将注意内存约束。


8

已经提到过,哈希表是O(n/m)平均值,如果n项的数量和m大小是。还已经提到,原则上,整个内容可以随O(n)查询时间折叠成一个单链表。(所有这些都假定计算哈希是恒定时间)。

但是,通常不会提到的是,至少有概率1-1/n(因此,对于1000件商品而言,有99.9%的几率),最大的水桶装满不会超过O(logn)!因此,匹配二叉搜索树的平均复杂度。(并且常数很好,约束更严格(log n)*(m/n) + O(1))。

要达到此理论上的界限,您需要使用一个相当不错的哈希函数(请参阅Wikipedia:通用哈希。它可以像一样简单a*x>>m)。当然,给您提供哈希值的人不知道您如何选择随机常数。

TL; DR:具有非常高的概率,哈希图的最坏情况下获取/放置复杂度为O(logn)


(并且请注意,这些都不是假设随机数据。该概率完全来自散列函数的选择)
Thomas Ahle 2014年

关于哈希映射中查找的运行时复杂性,我也有同样的问题。似乎应该将O(n)减为常数。1 / m是一个常数因子,因此被降低而剩下O(n)。
nickdu'4

4

我同意:

  • O(1)的一般摊销复杂度
  • 错误的hashCode()实现可能会导致多次冲突,这意味着在最坏的情况下,每个对象都将移至相同的存储桶,因此,如果每个存储桶都由a支持,则O(NList
  • 从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)。但是,对于其余部分,我们还需要计算hashCode()lookup元素,这意味着我们可能必须遍历算法中的数组和列表。

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

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

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

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


2

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

当有人说哈希图的获取/放置为O(1)时,应该真的说获取/放置所需的时间或多或少是恒定的,并且不取决于哈希图中的元素数量,只要哈希图可以出现在实际的计算系统上。如果问题超出了该范围,并且我们需要更大的哈希图,则过一会儿,随着我们用尽了可能描述的不同元素,描述一个元素的位数肯定也会增加。例如,如果我们使用哈希图存储32位数字,然后增加问题大小,以便在哈希图中有2 ^ 32位以上的元素,则单个元素的描述将超过32位。

描述单个元素所需的位数是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))位,对吗?
maaartinus
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.