Interviewers expect you to explain how a hash map works: a hash function picks a bucket, colliding keys share or probe for one, and the table resizes as it fills. Lookups are O(1) on average and O(n) in the worst case. You should also know what can be a key, and when a set is enough.
Operations and what they cost
| Operation | Average | Worst case | Note |
|---|---|---|---|
| Look up a key | O(1) | O(n) | The worst case is every key colliding. |
| Insert or update | O(1) amortized | O(n) | A resize rehashes everything, spread across many inserts. |
| Delete | O(1) | O(n) | |
| Iterate over all entries | O(n) | O(n) | Can depend on the table's capacity as well as its size. |
| Set union | O(a + b) | a and b are the sizes of the two sets. | |
| Set intersection | O(min(a, b)) | Walk the smaller set, look up in the larger. |
What each language actually documents differs, and it is worth saying the right thing for yours:
| Language | Types | What the documentation says | Source |
|---|---|---|---|
| Python | dict, set | O(1) as average-case times, O(n) in the worst case, when every key hashes to the same value. | Time complexity table |
| JavaScript | Map, Set | Sublinear on average in the number of elements. Hash tables or other mechanisms are allowed. | ECMAScript, Map objects |
| Java | HashMap, HashSet | Constant-time basic operations, assuming the hash function disperses the elements properly among the buckets. | HashMap |
The Python, JavaScript and Java pages have the full lists.
How a hash map works
A hash map is an array of buckets. To store a key, you hash it to a number, take that number modulo the array's length, and put the entry in that bucket. To find a key, you do the same and look there. That is why the cost does not depend on how many entries there are, as long as buckets stay short.
Two keys can land in the same bucket, which is a collision. Chaining keeps a small list in each bucket and walks it. Open addressing tries other slots in the table in a fixed order. Either way, if every key lands together, a lookup walks all of them, which is the O(n) worst case.
To keep buckets short, the table grows. The load factor is entries divided by buckets, and past a limit the table is rebuilt larger and every key rehashed. Java's HashMap defaults to a load factor of .75 and grows to about twice the buckets, and calls that default a good tradeoff between time and space. CPython's dict is described as a resizable hash table. A single insert can therefore be expensive, which is what amortized means here.
What can be a key
The rule behind every language's answer is that equal keys must hash the same, and a key's hash must not change while it is in the map.
- Python: a key must be hashable. Most immutable built-ins are, and lists and dicts are not. A tuple is hashable only if everything in it is, so use a tuple or a string for a composite key.
- JavaScript: a
Mapaccepts any value and compares keys with SameValueZero, so two different objects with the same contents are two keys. A plain object's keys must be strings or symbols, so numbers are converted. - Java: the hashCode contract says that if two objects are equal, their
hashCodemust match. OverrideequalswithouthashCodeand lookups quietly fail.
When to reach for it
- A brute force does a second loop to look something up. Replace the loop with a lookup.
- You are counting, grouping or deduplicating.
- You need to remember what you have already seen. If you only need yes or no, a set is enough; if you need to store something with it, use a map.
- You need a fast index over something else, such as a map from a key to a node in a list, as an LRU cache does.
- Not when you need order or ranges: there, consider a sorted structure.
Hash map interview questions
These are the conceptual questions that follow a hash map answer, and what a good reply covers. For coding questions, see the arrays and hashing pattern.
| The question | What a good answer covers |
|---|---|
| How does a hash map work? | Hash the key, find the bucket, handle collisions, grow when it fills. |
| Why is the worst case O(n)? | Every key can land in one bucket, and then a lookup walks them all. |
| What happens when it resizes? | A larger array, and every key rehashed. One insert costs O(n), so inserts are amortized O(1). |
| How are collisions handled? | Chaining or open addressing, and that long chains or clusters are what slow it down. |
| What makes a good key? | Equal keys hash equally, the hash never changes, and hashes spread out well. |
| Hash map or tree map? | Average O(1) with no order, or O(log n) with sorted keys and ranges. See hash map vs tree map. |
| Can you implement one? | An array of buckets, as in the Python example below. |
How it looks in Python, JavaScript and Java
In practice you use the built-in, so the first three do the same job in each language. The last is the from-scratch version an interviewer might ask for.
def word_counts(words):
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
return countsJavaScript
function wordCounts(words) {
const counts = new Map();
for (const w of words) counts.set(w, (counts.get(w) ?? 0) + 1);
return counts;
}Java
import java.util.*;
class Counting {
static Map<String, Integer> wordCounts(List<String> words) {
Map<String, Integer> counts = new HashMap<>();
for (String w : words) counts.merge(w, 1, Integer::sum);
return counts;
}
}class ChainedMap:
"""A hash map from scratch: an array of buckets, chaining on collision."""
def __init__(self):
self.buckets = [[] for _ in range(8)]
self.size = 0
def _bucket(self, key):
return self.buckets[hash(key) % len(self.buckets)]
def get(self, key, default=None):
for k, v in self._bucket(key):
if k == key:
return v
return default
def put(self, key, value):
bucket = self._bucket(key)
for i, (k, _) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # same key: replace
return
bucket.append((key, value))
self.size += 1
if self.size > len(self.buckets) * 3 // 4:
self._grow() # keep the load factor down
def _grow(self):
entries = [pair for bucket in self.buckets for pair in bucket]
self.buckets = [[] for _ in range(len(self.buckets) * 2)]
for k, v in entries:
self._bucket(k).append((k, v)) # the index depends on the lengthHow to explain the choice out loud
A second loop would make this O(n squared), so I'll use a hash map from each value to its index and replace that loop with a lookup. That's O(1) on average, so the pass is O(n) on average with O(n) extra space. The worst case is O(n) per lookup if every key lands in one bucket, but with a decent hash function and these keys I don't expect that. If I needed the keys in sorted order, I'd use a tree map instead and pay log n per operation.
Common mistakes
- Saying O(1) with no qualifier, then having no answer when asked about the worst case.
- Using a mutable key, such as a Python list, or a Java key whose fields change after it goes in.
- Overriding
equalsin Java withouthashCode, so equal objects land in different buckets. - Relying on iteration order. It is not the same across languages, so check what yours documents before you depend on it.
- Reaching for a map when a set is enough, or a set when you also need the count.
Where to go next
- Pattern: arrays and hashing.
- Worked problems: Match a Product Variant from Selected Options, Deduplicate a Member's Skills and Select the Top Unseen Feed Posts.
- All the figures in one place: the Big O cheat sheet.
- Practice saying it: a coding mock interview.