HashMap get/put complexity
It depends on many things. It's usually O(1), with a decent hash which itself is constant time... but you could have a hash which takes a long time to compute, and if there are multiple items in the hash map which return the same hash code, get
will have to iterate over them calling equals
on each of them to find a match.
In the worst case, a HashMap
has an O(n) lookup due to walking through all entries in the same hash bucket (e.g. if they all have the same hash code). Fortunately, that worst case scenario doesn't come up very often in real life, in my experience. So no, O(1) certainly isn't guaranteed - but it's usually what you should assume when considering which algorithms and data structures to use.
In JDK 8, HashMap
has been tweaked so that if keys can be compared for ordering, then any densely-populated bucket is implemented as a tree, so that even if there are lots of entries with the same hash code, the complexity is O(log n). That can cause issues if you have a key type where equality and ordering are different, of course.
And yes, if you don't have enough memory for the hash map, you'll be in trouble... but that's going to be true whatever data structure you use.
Time Complexity of HashMap methods
The source is often helpful: http://kickjava.com/src/java/util/HashMap.java.htm
remove:
O(1)size:
O(1)values:
O(n) (on traversal through iterator)
What is the time complexity of HashMap insertion and retrieval inside of a for loop?
It's O(n) for traversing the for loop for the elements and O(1) for the HashMap, the final answer is O(n), see more in here.
HashMap with O(log(N)) time complexity operations
Edited to correct the answer after the helpful comments of The Vee and Tom Elias.
You can't implement addToKey
and addToValue
by iterating over all the elements, since that requires, as you realized, linear time.
Instead, you can keep a keyDelta
and valueDelta
int
instance variables that hold the value (or the sum of all values) that should be added to each key and value respectively. This is assuming addToKey
and addToValue
are supposed to add a numeric value to the keys or values.
addToKey
and addToValue
will only need to update those instance variables, which would take O(1)
.
The get(x)
operation will search for the key x - keyDelta
and return its corresponding value after adding valueDelta
to it.
Note that add(x,y)
also requires a change, since key-value pairs added to the Map should not be affected by previous calls to addToKey
or addToValue
. This can be achieved if insert(x,y)
will actually insert the pair x - keyDelta, y - valueDelta
to the map.
If you use a TreeMap
to implement the mapping of keys to values, get
and insert
would take O(logN)
.
What is the complexity of HashMap#replace?
tl:dr
HashMap#replace
runs in O(1)
amortized;
and under the premise that the map is properly balanced, which Java takes care of during your put
and remove
calls, also non-amortized.
Non-amortized
The fact whether it also holds for non-amortized analysis hinges on the question regarding the implemented self-balancing mechanism.
Basically, due to replace
only changing the value which does not influence hashing and the general structure of the HashMap, replacing a value will not trigger any re-hashing or re-organization of the internal structure.
Hence we only pay for the cost of locating the key
, which depends on the bucket size.
The bucket size, if the map is properly self-balanced, can be considered a constant. Leading to O(1)
for replace
also non-amortized.
However, the implementation triggers self-balancing and re-hashing based on heuristic factors only. A deep analysis of that is a bit more complex.
So the reality is probably somewhere in between due to the heuristics.
Implementation
To be sure, let us take a look at the 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;
}
The method afterNodeAccess
is a dummy for subclasses and is empty in HashMap
.
Everything except getNode
runs in O(1)
trivially.
getNode
getNode
is the canonical implementation of locating an entry in a HashMap
, which we know runs in O(1)
for a proper self-balancing map, like Javas implementation. Let's take a look at the 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;
}
This method basically computes the hash hash = hash(key)
, then looks up the hash in the table
first = tab[(n - 1) & (hash = hash(key))]
and starts iterating through the data structure stored in the bucket.
Regarding the data structure for the bucket, we have a little branching going on at if (first instanceof TreeNode)
.
Bucket
The buckets are either simple implicitly linked lists or red-black-tree.
Linked List
For the linked list, we have a straightforward iteration
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
which obviously runs in O(m)
with m
being the size of the linked list.
Red-Black-Tree
For the red-black-tree, we have
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
Lookup in a red-black-tree is O(log m)
, with m
being the size of the tree.
Bucket size
Javas implementation makes sure to re-balance the buckets by rehashing if it detects that it gets out of hands (you pay for that on each modifying method like put
or remove
).
So in both cases we can consider the size of the buckets as constant or, due to the heuristics involved with self-balancing, close to a constant.
Conclusion
Having the buckets at constant size, effectively, makes getNode
run in O(1)
, leading to replace
running in O(1)
as well.
Without any self-balancing mechanism, in worst case it would degrade to O(n)
if a linked list is used and O(log n)
for a red-black-tree (for the case that all keys yield a hash collision).
Feel free to dig deeper into the code but it gets a bit more complex down there.
Time complexity for HashMap put/get inside for?
O(3N) and O(N) would be just considered O(N).
I guess, maybe getOrDefault()
would be an option, if I'd correctly recall:
for (int i = 0; i < n.length(); i++) {
int aux = n[i];
map.put(aux, map.getOrDefault(aux, 0) + 1);
}
and your time and memory complexity would be both O(N).
I think another more readable version would be:
for (int i = 0; i < n.length(); i++) {
int aux = n[i];
map.put(aux, -~map.getOrDefault(aux, 0));
}
-~x
is just a bitwise operation for x + 1
(-~x = x + 1
). However, you can use whichever you would be comfortable.
Thanks to ciamej in the comment!
Related Topics
Cut Out Image in Shape of Text
Display Indeterminate Jprogressbar While Batch File Runs
Using Env Variable in Spring Boot's Application.Properties
No Compiler Is Provided in This Environment. Perhaps You Are Running on a Jre Rather Than a Jdk
Java Integer Compareto() - Why Use Comparison VS. Subtraction
Dynamic Spring Data JPA Repository Query with Arbitrary and Clauses
When Should We Call System.Exit in Java
Can't Add Value to the Java Collection with Wildcard Generic Type
How to Maintain Jtable Cell Rendering After Cell Edit
Differencebetween Serializable and Externalizable in Java
What Exactly Is Field Injection and How to Avoid It
Intellij Inspection Gives "Cannot Resolve Symbol" But Still Compiles Code
Why Does the Tostring Method in Java Not Seem to Work for an Array