Java hashmap 搜索真的是 O(1) 吗?

Posted

技术标签:

【中文标题】Java hashmap 搜索真的是 O(1) 吗?【英文标题】:Is a Java hashmap search really O(1)? 【发布时间】:2010-11-06 12:22:27 【问题描述】:

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

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

谁能解释一下他们是否 O(1),如果是,他们是如何做到的?

【问题讨论】:

我知道这可能不是一个答案,但我记得 Wikipedia 有一个 very good article 关于这个。不要错过performance analysis 部分 大 O 表示法为您正在执行的特定类型的分析提供上限。您仍然应该指定您是否对最坏情况、平均情况等感兴趣。 【参考方案1】:

在 Java 中,HashMap 是如何工作的?

使用hashCode定位对应的bucket[在buckets容器模型内]。 每个存储桶都是驻留在该存储桶中的项目的列表(或从 Java 8 开始的树)。 项目被一一扫描,使用equals进行比较。 添加更多项目时,一旦达到特定负载百分比,就会调整 HashMap 的大小。

因此,有时它必须与几个项目进行比较,但一般来说,它比 O(n) 更接近 O(1)。 出于实际目的,这就是您应该知道的全部内容。

【讨论】:

好吧,既然 big-O 应该指定限制,它是否更接近 O(1) 没有区别。即使 O(n/10^100) 仍然是 O(n)。我理解你关于效率降低比率的观点,但这仍然使算法处于 O(n)。 Hash-maps 分析通常是在平均情况下,即 O(1)(有合谋)在最坏的情况下,你可以有 O(n),但通常情况并非如此。关于差异 - O(1) 意味着无论图表上的项目数量如何,您都可以获得相同的访问时间,并且通常是这种情况(只要表格的大小和 'n ') 同样值得注意的是,它仍然完全是 O(1),即使桶的扫描需要一段时间,因为其中已经有一些元素。只要桶具有固定的最大大小,这只是一个与 O() 分类无关的常数因素。但是当然可以添加更多带有“相似”键的元素,这样这些桶就会溢出,你不能再保证一个常量了。 @sth 为什么桶会有一个固定的最大大小!?【参考方案2】:

HashMap 中的元素存储为链表(节点)数组,数组中的每个链表代表一个桶,用于一个或多个键的唯一哈希值。 在 HashMap 中添加 entry 时,通过 key 的 hashcode 来确定 bucket 在数组中的位置,类似于:

location = (arraylength - 1) & keyhashcode

这里的 & 表示按位与运算符。

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

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

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

在 java 8 的情况下,如果大小增长到 8 以上,Linked List 存储桶将被替换为 TreeMap,这会将最坏情况的搜索效率降低到 O(log n)。

【讨论】:

【参考方案3】:

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

你说得对,hash map 并不是真正的O(1),严格来说,因为随着元素的数量变得任意大,最终你将无法在恒定时间内搜索(并且定义了 O-notation就可以任意大的数字而言)。

但这并不意味着实时复杂度为O(n)——因为没有规定必须将桶实现为线性列表。

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

【讨论】:

【参考方案4】:

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

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

【讨论】:

常数 alpha 表示什么? java.util.HashMap 中,alpha 常量与构造函数上的 负载因子 参数相关。虽然不完全是,因为桶的数量是通过HashMap 在调整大小时选择大小的方式量化的。 (此分析还假设桶的键分布大致均匀;即它是平均情况复杂度,而不是最坏情况复杂度。)【参考方案5】:

HashMap 的一个特殊特性是与平衡树不同,它的行为是概率性的。在这些情况下,根据最坏情况事件发生的概率来讨论复杂性通常是最有帮助的。对于哈希映射,这当然是关于映射恰好有多满的冲突的情况。碰撞很容易估计。

p碰撞 = n / 容量

因此,即使是少量元素的哈希映射也很可能至少会发生一次冲突。大 O 表示法允许我们做一些更有说服力的事情。观察任意的固定常数 k。

O(n) = O(k * n)

我们可以使用这个特性来提高哈希映射的性能。相反,我们可以考虑最多 2 次碰撞的概率。

p碰撞 x 2 = (n / 容量)2

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

p碰撞 x k = (n / 容量)k

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

我们通过说哈希映射具有 O(1) 访问权限来讨论这个问题很有可能

【讨论】:

实际上,上面所说的是,对于 N 的非极端值,O(log N) 效应被固定开销所掩盖。 从技术上讲,您给出的那个数字是碰撞次数的预期值,可以等于单次碰撞的概率。 这类似于摊销分析吗? 您的概率假设哈希码分布良好。根据您的数据和您的 hashCode 方法,分布可能是不利的,因此您的论点会失败。 @OleV.V. HashMap 的良好性能始终取决于散列函数的良好分布。您可以通过在输入中使用加密哈希函数来以更好的哈希质量换取哈希速度。【参考方案6】:

仅在理论上,当哈希码总是不同并且每个哈希码的桶也不同时,O(1) 才会存在。否则,它的顺序是恒定的,即在 hashmap 递增时,它的搜索顺序保持不变。

【讨论】:

【参考方案7】:

当然,hashmap 的性能取决于给定对象的 hashCode() 函数的质量。但是,如果该函数被实现为碰撞的可能性非常低,它将具有非常好的性能(这在每一种可能的情况下都不是严格的 O(1),但在 大多数案例)。

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

【讨论】:

“在大多数情况下是这样”。更具体地说,总时间将趋向于 K 乘以 N(其中 K 是常数),因为 N 趋于无穷大。 这是错误的。哈希表中的索引将通过hashCode % tableSize 确定,这意味着肯定会发生冲突。您没有充分利用 32 位。这就是哈希表的意义所在……您可以将大的索引空间缩减为一个小的索引空间。 "你保证不会有冲突" 不,你不是因为地图的大小小于散列的大小:例如,如果地图的大小是 2,如果/当我尝试插入三个元素时,就会保证发生冲突(不管哈希是什么)。 但是如何在 O(1) 中从键转换为内存地址?我的意思是像 x = array["key"]。关键不是内存地址,所以它仍然必须是 O(n) 查找。 “我相信如果你不实现hashCode,它会使用对象的内存地址”。它可以使用它,但标准 Oracle Java 的默认 hashCode 实际上是存储在对象头中的 25 位随机数,因此 64/32 位无关紧要。【参考方案8】:

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

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

【讨论】:

【参考方案9】:

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

【讨论】:

实际应用中没有。一旦您使用字符串作为键,您就会注意到并非所有哈希函数都是理想的,而且有些非常慢。【参考方案10】:

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

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

【讨论】:

【参考方案11】:

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

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

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

【讨论】:

【参考方案12】:

请记住,o(1) 并不意味着每次查找只检查单个项目 - 它意味着检查的项目的平均数量保持不变 w.r.t。容器中的项目数。因此,如果在包含 100 个项目的容器中找到一个项目平均需要 4 次比较,那么在包含 10000 个项目的容器中找到一个项目也应该平均需要 4 次比较,对于任何其他数量的项目(总是有有点差异,尤其是在哈希表重新散列的点周围,以及当项目数量非常少时)。

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

【讨论】:

【参考方案13】:

这取决于您选择的避免冲突的算法。如果您的实现使用单独的链接,那么最坏的情况会发生,即每个数据元素都被散列到相同的值(例如散列函数的选择不当)。在这种情况下,数据查找与链表上的线性搜索没有什么不同,即 O(n)。但是,这种情况发生的概率可以忽略不计,并且查找最佳和平均情况保持不变,即 O(1)。

【讨论】:

【参考方案14】:

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

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

【讨论】:

假设运行时间受查找时间限制。在实践中你会发现很多情况下哈希函数提供了边界(字符串)【参考方案15】:

您似乎将最坏情况的行为与平均情况(预期)的运行时间混为一谈。对于一般的哈希表,前者确实是 O(n)(即不使用完美的哈希),但这在实践中很少相关。

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

【讨论】:

我一直认为上限是最坏的情况,但似乎我错了 - 您可以将上限用于平均情况。因此,声称 O(1) 的人似乎应该明确表示这是针对普通情况的。最坏的情况是一个数据集,其中有许多碰撞使其成为 O(n)。现在说得通了。 您可能应该明确表示,当您对平均情况使用大 O 表示法时,您正在谈论预期运行时函数的上限,这是一个明确定义的数学函数。否则你的回答没有多大意义。 gmatt:我不确定我是否理解您的反对意见:big-O 表示法是函数定义的上限。我还能说什么? 通常在计算机文献中你会看到大 O 表示法表示算法的运行时或空间复杂度函数的上限。在这种情况下,上限实际上是期望本身不是函数,而是函数上的运算符(随机变量),实际上是一个积分(lebesgue)。不应该考虑可以绑定这样一个东西的事实理所当然的,不是微不足道的。

以上是关于Java hashmap 搜索真的是 O(1) 吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 hashmap 查找是 O(1) 即常数时间?

Java 有反向查找的 HashMap 吗?

HashMap都在用,原理你真的了解吗?

将数据存储为具有空/空值的 HashMap 中的键是一个好主意吗?

HashMap.get 方法如何工作[重复]

HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set