tl:dr
HashMap#replace 在O(1) amortized 中运行;
并且在地图适当平衡的前提下,Java 在您的 put 和 remove 调用期间负责,也是非摊销。
非摊销
它是否也适用于非摊销分析取决于关于实施的自平衡机制的问题。
基本上,由于replace只是改变value,不影响散列和HashMap的一般结构,替换一个值不会触发任何重新散列或重组内部结构。
因此我们只支付定位key 的费用,这取决于存储桶大小。
如果地图是适当的自平衡,桶大小可以被认为是一个常数。导致O(1) 的replace 也未摊销。
但是,该实施仅基于启发式因素触发自平衡和重新散列。对此的深入分析有点复杂。
因此,由于启发式方法,现实可能介于两者之间。
实施
当然,让我们看看current implementation(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 中定位条目的规范实现,我们知道它在O(1) 中运行以获得适当的自平衡映射,如Java 实现。一起来看看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),然后在tablefirst = tab[(n - 1) & (hash = hash(key))]中查找哈希并开始遍历存储在bucket中的数据结构。
关于存储桶的数据结构,我们在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);
显然在O(m) 中运行,m 是链表的大小。
红黑树
对于red-black-tree,我们有
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
在红黑树中的查找是O(log m),m 是树的大小。
桶大小
Java 实现确保在检测到它失控时通过重新散列重新平衡存储桶(您需要为每个修改方法(如 put 或 remove)付费)。
因此,在这两种情况下,我们都可以将桶的大小视为常数,或者由于涉及自平衡的启发式方法,接近常数。
结论
有效地使存储桶大小不变,使getNode 在O(1) 中运行,从而导致replace 也在O(1) 中运行。
如果没有任何自平衡机制,在最坏的情况下,如果使用链表,它将降级为O(n),而对于红黑树,它将降级为O(log n)(对于所有键都会产生哈希冲突的情况)。
您可以随意深入挖掘代码,但那里会变得更复杂。