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

Posted

技术标签:

【中文标题】为啥 hashmap 查找是 O(1) 即常数时间?【英文标题】:Why hashmap lookup is O(1) i.e. constant time?为什么 hashmap 查找是 O(1) 即常数时间? 【发布时间】:2013-03-06 08:59:22 【问题描述】:

如果我们从 Java 的角度来看,那么我们可以说 hashmap 查找需要恒定的时间。但是内部实现呢?它仍然需要通过特定的桶(匹配哪个键的哈希码)来搜索不同的匹配键。那为什么我们说哈希图查找需要恒定的时间呢?请解释一下。

【问题讨论】:

Can hash tables really be O(1) 的可能重复项 【参考方案1】:

在对所使用的散列函数的适当假设下,我们可以说散列表查找需要 预期 O(1) 时间(假设您使用的是标准散列方案,如线性探测或链式哈希)。这意味着平均而言,哈希表执行查找的工作量最多是某个常数。

直观地说,如果你有一个“好”的散列函数,你会期望元素在整个散列表中或多或少地分布均匀,这意味着每个桶中的元素数量将接近被划分的元素数量通过桶的数量。如果哈希表实现将这个数字保持在较低水平(例如,每次元素与存储桶的比率超过某个常数时添加更多存储桶),那么预期完成的工作量最终会成为选择哪个存储桶的一些基线工作量应该被扫描,然后做“不要太多”的工作来查看那里的元素,因为预计该桶中的元素数量只会是恒定的。

这并不意味着哈希表具有保证 O(1) 行为。事实上,在最坏的情况下,散列方案会退化,所有元素最终都会放在一个桶中,在最坏的情况下,查找需要花费时间 Θ(n)。这就是为什么设计好的散列函数很重要。

如需更多信息,您可能需要阅读算法教科书以了解哈希表为何如此高效地支持查找的正式推导。这通常包含在典型的大学算法和数据结构课程中,并且在线上有很多很好的资源。

有趣的事实:在某些类型的哈希表(布谷鸟哈希表、动态完美哈希表)中,最坏情况元素的查找时间是 O(1)。这些哈希表的工作原理是保证每个元素只能位于几个固定位置之一,插入有时会在元素周围争先恐后地试图使一切都合适。

希望这会有所帮助!

【讨论】:

感谢您简单明了的回答。它巧妙地回答了我的问题。 您正确回答以获得价值。但是桶的位置呢?我们知道对于每个键(假设散列函数太好,为每个键生成不同的散列值),将创建一个表来定位桶,即链表(直到 java 7)。我的问题是,如果有 1000K 个桶有 1000K 个不同的键值,hashmap 如何确保它会给出 o(1) 甚至查找桶位置?希望我清楚问题。谢谢。 @Sam 存储桶通常存储为一个数组(可以是原始数组,也可以是支持有效随机访问的 ArrayList 之类的包装器。这样,在计算了哈希函数之后,您可以在 O(1) 时间(与桶数无关)中跳转到正确的桶。【参考方案2】:

关键在文档中的这个语句中:

如果要在一个 HashMap 实例中存储许多映射,则创建具有足够大容量的映射将比让它根据需要执行自动重新散列以增加表来更有效地存储映射。

负载因子是哈希表在其容量自动增加之前允许达到的程度的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,对哈希表进行重新哈希(即重建内部数据结构),使哈希表的桶数大约增加一倍。

http://docs.oracle.com/javase/6/docs/api/java/util/HashMap.html

如果超过负载因子,内部bucket结构实际上会被重建,考虑到get摊销成本设为 O(1)。

请注意,如果重新构建内部结构,则会引入可能为 O(N) 的性能损失,因此可能会有相当多的 getput摊销成本 再次接近 O(1) 之前需要。因此,请适当规划初始容量和负载系数,这样既不会浪费空间,也不会触发可避免的内部结构重建。

【讨论】:

【参考方案3】:

哈希表不是 O(1)。

通过鸽巢原理,你不可能比 O(log(n)) 更好地进行查找,因为你需要每个项目的 log(n) 位来唯一标识 n 个项目。

哈希表 似乎 是 O(1),因为它们有一个小的常数因子,加上它们在 O(log(n)) 中的“n”被增加到这样的程度,对于许多实际应用程序,它与您正在使用的实际项目的数量无关。然而,大 O 表示法并不关心这个事实,而且它是(理所当然的,荒谬地普遍地)滥用该表示法来调用哈希表 O(1)。

因为虽然您可以在哈希表中存储一百万或十亿个项目,但仍然获得与单个项目哈希表相同的查找时间......如果您正在处理大约非十亿或 googleplex 项目,您将失去该能力。您永远不会真正使用 nonillion 或 googleplex 项目这一事实对于大 O 表示法并不重要。

实际上,哈希表性能可能比数组查找性能更差。是的,这也是 O(log(n)),因为你不能做得更好。

基本上,现实世界的计算机使每个数组查找大小小于其芯片位大小的数组与其最大的理论上可用的数组一样糟糕,并且由于 hastables 是在数组上执行的巧妙技巧,这就是为什么您似乎 得到 O(1)

【讨论】:

【参考方案4】:

还要跟进 templatetypedef 的 cmets:

哈希表的恒定时间实现可以是一个哈希映射,您可以使用它实现一个布尔数组列表,该列表指示特定元素是否存在于存储桶中。但是,如果您正在为您的 hashmap 实现一个链表,最坏的情况是需要您遍历每个存储桶并且必须遍历列表的末端。

【讨论】:

以上是关于为啥 hashmap 查找是 O(1) 即常数时间?的主要内容,如果未能解决你的问题,请参考以下文章

如何设计并实现一个线程安全的 Map

为啥常数总是从大 O 分析中删除?

hashmap 1.7

Java HashMap 如何为“get”操作执行恒定时间查找 O(1)?

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

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