HashMap#replace 的复杂度是多少?

a_c*_*ent 5 java algorithm hashmap time-complexity

我想知道replace(Key , Value)for a HashMapis的复杂性是什么。

我最初的想法是O(1)因为它是O(1)获取值,我可以简单地替换分配给键的值。

我不确定是否应该考虑在 java 中实现的大型哈希图中可能存在的冲突java.util

Zab*_*uza 6

电话:博士

HashMap#replaceO(1) 摊销

并且在地图适当平衡的前提下,Java 在您的putremove调用期间负责,也未摊销

未摊销

它是否也适用于非摊销分析取决于有关实施的自平衡机制的问题

基本上,由于replace只改变不影响散列和HashMap中的一般结构,替换值将不会触发任何重新散列或内部结构的重新组织。

因此,我们只支付定位 的成本key,这取决于桶的大小

桶大小,如果地图适当地自平衡,可以被认为是一个常数。从而O(1)replace也非摊销。

但是,该实现仅基于启发式因素触发自平衡和重新散列。对此的深入分析有点复杂。

因此,由于启发式方法,现实可能介于两者之间。


执行

可以肯定的是,让我们看看当前的实现(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;
}
Run Code Online (Sandbox Code Playgroud)

该方法afterNodeAccess是子类的虚拟方法,并且在HashMap. 除了微不足道的getNode运行之外的一切O(1)

getNode

getNode是在 a 中定位条目的规范实现HashMap,我们知道它运行在O(1)适当的自平衡映射中,例如 Java 实现。让我们来看看代码

/**
 * 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;
}
Run Code Online (Sandbox Code Playgroud)

此方法基本上计算 hash hash = hash(key),然后在 中查找 hashtable 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);
Run Code Online (Sandbox Code Playgroud)

这显然O(m)m链表的大小有关。

红黑树

对于红黑树,我们有

return ((TreeNode<K,V>)first).getTreeNode(hash, key);
Run Code Online (Sandbox Code Playgroud)

在红黑树中查找是O(log m)m是树的大小。

铲斗尺寸

Javas 实现确保在检测到失控时通过重新散列来重新平衡存储桶(您为每个修改方法(如putremove)支付费用)。

因此,在这两种情况下,我们都可以将桶大小视为常数,或者由于涉及自平衡的启发式方法,接近常数。

结论

具有固定大小的水桶,有效,使得getNode运行O(1),导致replace在运行O(1)也是如此。

如果没有任何自平衡机制,在最坏的情况下,它会降级为O(n)如果使用链表和O(log n)红黑树(对于所有键都产生哈希冲突的情况)。

随意深入研究代码,但它在那里变得有点复杂。