我想知道对于 HashMap
中的 replace(Key, Value)
方法,其时间复杂度是多少。
我的初步想法是它的时间复杂度为 O(1)
,因为获取值的时间复杂度为 O(1)
,而且我可以简单地替换与键相关联的值。
但我不确定是否应该考虑在使用 java.util
实现的大型 hashmap 中可能出现的冲突问题。
我想知道对于 HashMap
中的 replace(Key, Value)
方法,其时间复杂度是多少。
我的初步想法是它的时间复杂度为 O(1)
,因为获取值的时间复杂度为 O(1)
,而且我可以简单地替换与键相关联的值。
但我不确定是否应该考虑在使用 java.util
实现的大型 hashmap 中可能出现的冲突问题。
HashMap#replace
的时间复杂度为 O(1)
平均;
在映射表平衡的前提下,Java在您的put
和remove
调用期间会进行处理,这也是非平均的。
是否也适用于非平均分析的事实取决于所实现的自平衡机制。
基本上,由于replace
只改变不影响哈希和HashMap的一般结构的值,替换值不会触发任何重新哈希或重新组织内部结构。
因此,我们只需要支付定位key
的成本,这取决于桶大小。
replace
的时间复杂度为O(1)
,也不是摊销的。为确保准确性,让我们来看看当前的实现(Java 16):
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
afterNodeAccess
方法是子类的虚拟方法,在HashMap
中为空。除了getNode
方法外,其他所有方法都可以轻松地运行在O(1)
时间复杂度。
getNode
getNode
是在HashMap
中查找条目的规范实现,对于像Java实现的适当自平衡映射,我们知道它运行在O(1)
时间复杂度。让我们来看一下code:
/**
* Implements Map.get and related methods.
*
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
hash = hash(key)
,然后在 table
中查找哈希 first = tab[(n - 1) & (hash = hash(key))]
并开始迭代存储在桶中的数据结构。
关于桶的数据结构,我们在 if (first instanceof TreeNode)
处进行了一些分支处理。
桶可以是简单的隐式链接列表或红黑树。
对于链接列表,我们有一个直接的迭代。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
这显然在链表大小为 m
时运行时间为 O(m)
。
对于 红黑树,我们有:
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
put
和 remove
过程中需要承担冲突的成本,而在 containsKey
、get
和 replace
过程中则不需要。请参考最后一段内容了解原因。简而言之,由于重新散列和自平衡,桶大小可以被视为恒定。 - Zabuzard100
时,您会增加并重新散列整个表。这样,您可以将桶大小视为常量,独立于 n
,尽管存在冲突。自平衡会处理这些问题。话虽如此,当前的实现使用启发式方法来确定自平衡因素,因此处于两个世界之间。 - Zabuzard你是对的,主要成本是查找,这是摊销 O(1)。
一旦我们找到正确的位置,用新值替换相关联的值是 O(1)。但是查找只有摊销的 O(1)。
正如 Zabuzard 的 错误 答案中附带的代码所示,Java HashMap 使用了经典方法,如果你很幸运(你要查找的条目是桶中的第一个条目),那么查找复杂度是 O(1)。
如果你不太幸运或者你的哈希函数质量较差(假设最坏情况,所有元素都映射到相同的哈希键),为了避免遇到遍历普通链表的可怕的 O(n) 复杂度,Java 实现使用 TreeMap 提供 O(log n) 复杂度。
因此,如果正确使用 Java 的 hashmap 应该基本上得到 O(1) 的替换,如果使用不正确,则会优雅地退化到 O(log n) 的复杂度。阈值在 TREEIFY 中(例如现代实现中的 value 是 8)。
请查看源代码中的这些实现说明:https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/util/HashMap.java#L143-L231
基础知识:
java.util.HashMap
会自动调整大小以匹配给定数量的元素HashMap
实现在桶内使用树形结构(Node
和 TreeNode
)在一次替换/包含/放置/获取操作中,桶碰撞,
此外,最坏情况下,哈希碰撞:
Node
实现类似于 LinkedList
LinkedList
类似的搜索) O(n/2) = O(n) 的复杂度。hashCode()
另外,请查看注释和代码本身:https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/HashMap.java
tableSizeFor(int cap)
getNode()
具体来说:
2^n - 1
first = tab[(n - 1) & hash])
其中 'first' 是桶为了说明如何自己研究这个问题,我编写了一些代码,展示了最坏情况(哈希冲突)下的行为:
import java.util.HashMap;
public class TestHashMapCollisions {
static class C {
private final String mName;
public C(final String pName) {
mName = pName;
}
@Override public int hashCode() {
return 1;
}
@Override public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final C other = (C) obj;
if (mName == null) {
if (other.mName != null) return false;
} else if (!mName.equals(other.mName)) return false;
return true;
}
}
public static void main(final String[] args) {
final HashMap<C, Long> testMap = new HashMap<>();
for (int i = 0; i < 5; i++) {
final String name = "name" + i;
final C c = new C(name);
final Long value = Long.valueOf(i);
testMap.put(c, value);
}
final C c = new C("name2");
System.out.println("Result: " + testMap.get(c));
System.out.println("End.");
}
}
步骤:
System.out.println("Result: " + testMap.get(c));
HashMap
实现HashMap.getNode()
的第一行(Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
)HashMap
内停止提示:(您可以立即在HashMap内设置断点,但这会导致一些混乱,因为当JVM初始化时经常使用HashMap
,所以您会先遇到很多不需要的停止,然后才能测试您的代码)
O(1)
摊销,就像containsKey
、put
或remove
一样。如果不进行摊销分析,它可能是O(n)
,因为任何更改都可能触发重新哈希,潜在地涉及所有条目。但是没有人关心非摊销分析。 - Zabuzardreplace
只改变 值,而值并不受哈希的影响。因此,它实际上与get
或contains
具有相同的复杂度。 - Zabuzard