我们习惯说HashMap
get/put
运算是O(1)。但是,这取决于哈希实现。默认对象哈希实际上是JVM堆中的内部地址。我们确定声称get/put
O(1)是否足够好?
可用内存是另一个问题。据我从javadocs理解,HashMap
load factor
应该是0.75。如果我们在JVM中没有足够的内存并且load factor
超出限制怎么办?
因此,似乎无法保证O(1)。是有意义还是我想念什么?
我们习惯说HashMap
get/put
运算是O(1)。但是,这取决于哈希实现。默认对象哈希实际上是JVM堆中的内部地址。我们确定声称get/put
O(1)是否足够好?
可用内存是另一个问题。据我从javadocs理解,HashMap
load factor
应该是0.75。如果我们在JVM中没有足够的内存并且load factor
超出限制怎么办?
因此,似乎无法保证O(1)。是有意义还是我想念什么?
Answers:
这取决于很多事情。这通常是 O(1),一个体面的哈希它本身是固定的时间...但你可以有一个哈希这需要很长的时间来计算,而如果在散列图多个项目,其中返回相同的散列码,get
将不得不遍历他们,呼吁他们equals
每个人寻找比赛。
在最坏的情况下,HashMap
由于遍历同一哈希存储桶中的所有条目(例如,如果它们都具有相同的哈希码),则a 具有O(n)查找。幸运的是,根据我的经验,最坏的情况在现实生活中不会经常出现。因此,不能,O(1)当然不能得到保证-但是通常这是您在考虑使用哪种算法和数据结构时应该假定的。
在JDK 8中,HashMap
已经进行了调整,以便可以比较键的排序方式,然后将任何人口稠密的存储桶都实现为树,因此即使有很多具有相同哈希码的条目,复杂度也为O(log n)。当然,如果您的键类型的相等性和顺序不同,则可能会导致问题。
是的,如果您没有足够的内存来存储哈希映射,则会遇到麻烦……但是,无论使用哪种数据结构,这都是正确的。
put
是“摊销O(1)”-通常是O(1),偶尔是O(n)-但很少能平衡。
我不确定默认的哈希码是否为地址-前一阵子我读过OpenJDK源代码来生成哈希码,但我记得它有点复杂。也许仍然不能保证良好的发行。但是,从某种程度上讲,没有什么用,因为在哈希图中用作键的类很少使用默认的哈希码-它们提供了自己的实现,应该很好。
最重要的是,您可能不知道(同样,这是基于阅读源的-不能保证)是HashMap在使用哈希之前先对其进行搅拌,将整个单词的熵混合到最低位,也就是最低位除了最大的哈希图以外的所有图都需要。尽管我无法想到任何常见的情况,但这样做有助于处理那些本身并不专门执行的哈希。
最后,当表超载时发生的事情是,它退化为一组并行链接列表-性能变为O(n)。具体而言,所遍历的链路数平均将为负载因子的一半。
已经提到过,哈希表是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)
。
我同意:
hashCode()
实现可能会导致多次冲突,这意味着在最坏的情况下,每个对象都将移至相同的存储桶,因此,如果每个存储桶都由a支持,则O(N)List
。HashMap
动态地将每个存储桶中使用的Nodes(链接列表)替换为TreeNodes(当列表大于8个元素时为红黑树),从而导致O(logN)性能最差。但是,如果我们想做到100%精确,这并不是全部。hashCode()
密钥的实现和类型Object
(不可变/已缓存或为Collection)也可能严格地影响实际的复杂性。
让我们假设以下三种情况:
HashMap<Integer, V>
HashMap<String, V>
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”,请参阅此处,在这种情况下,备注可能无济于事。
实际上,它是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))
位,对吗?