O(1) average lookup. The most-used data structure in coding interviews โ and in production code. If you know one thing cold, make it this.
โฌก IntermediateA hash map (also called a hash table or dictionary) stores key-value pairs โ you associate a key (like a username) with a value (like their profile data). You give it a key, it gives you the value โ in O(1) average time. That's the magic.
Imagine a library where instead of searching every shelf, you have a formula that tells you exactly which shelf a book is on based on its title. You skip straight there. That formula is the hash function.
A hash set is the same idea, but it only stores keys โ no values. It answers one question: "Is this element in the set?" Python's set and JavaScript's Set are hash sets.
The hash table was invented in 1953 by Hans Peter Luhn at IBM, who described it in an internal memo. Around the same time (1953), Gene Amdahl, Elaine McGraw, and Arthur Samuel independently implemented hash tables for an IBM 701 assembler. The technique was published more broadly by Arnold Dumey in 1956.
The reason this was a breakthrough: before hash tables, the fastest way to look up a value by key was a balanced binary search tree โ O(log n). Hash tables offered O(1) average-case lookup. For a million entries, that's the difference between ~20 comparisons and 1. The trade-off? You need a good hash function, and worst-case performance is O(n). But the average case is so good that hash tables became the most widely used data structure in all of software engineering.
Hash Function โ takes a key (string, number, whatever) and converts it to an integer. That integer, taken modulo the array size, becomes the index (also called a bucket) where the value is stored. A good hash function distributes keys evenly across indices, minimizing collisions.
Collision โ when two different keys hash to the same index. This will happen (the pigeonhole principle guarantees it: if you have more possible keys than array slots, some must share). How you handle collisions defines the hash map's behavior.
Load Factor โ the ratio of stored items to array size (n/capacity). When it gets too high (typically > 0.75), performance degrades because collisions increase. The hash map resizes โ doubles the array and rehashes every existing key. That single resize is O(n), but it happens rarely enough that insertions are still amortized O(1) (the expensive operation's cost is "amortized" โ spread across all the cheap operations).
A hash function takes a key and produces an index. For a string key like "cat":
99 ร 31ยฒ + 97 ร 31 + 116 = 98,194 (using prime 31 as multiplier)98194 % 16 = 2Why prime numbers? They distribute hash values more evenly. If you used powers of 2 as multipliers, the lower bits of your keys would dominate, causing clusters. The prime 31 is used by Java's String.hashCode() and has become the de facto standard โ it's small enough to be fast (multiplying by 31 is the same as (x << 5) - x, which the CPU optimizes to a shift and subtract) and large enough to spread values well.
Chaining (Separate Chaining) โ each bucket holds a linked list (or, in modern implementations, a dynamic array). When two keys hash to the same index, both go into that bucket's list. To find a key, hash it, go to that bucket, then walk the list. If the load factor stays low, these lists are short (1-2 items on average).
Open Addressing โ instead of lists, when a collision happens, you probe for the next empty slot:
Java's HashMap uses chaining (and switches from a linked list to a red-black tree when a bucket exceeds 8 items). Python's dict uses open addressing with a custom probing sequence. Both work well in practice โ chaining is simpler to implement and handles high load factors gracefully; open addressing has better cache locality (CPU-friendly memory access patterns) since everything lives in one contiguous array.
When the load factor crosses the threshold (0.75 in most implementations), the map allocates a new array โ usually double the size โ and rehashes every existing key. Every key gets a new index because the modulo value changed (hash % 16 vs hash % 32 gives different results). This single resize is O(n) for that one operation, but since it only happens after n/2 insertions, the cost per insertion averages out to O(1).
This is why hash maps use powers of 2 for capacity โ modulo with a power of 2 is just a bitwise AND, which is extremely fast: hash % 16 is the same as hash & 15.
A hash set is literally a hash map where you only care about keys. The value is either absent or a dummy placeholder. Same hash function, same collision handling, same resizing. When you call set.add(x), it's doing map[x] = true under the hood.
Watch the hash function in action. Put a key-value pair, see which bucket it lands in, and observe collisions creating chains.
| Operation | Average | Worst | Notes |
|---|---|---|---|
| Put (insert/update) | O(1) | O(n) | Worst case: all keys hash to same bucket |
| Get (lookup) | O(1) | O(n) | Same reason โ degenerate chain |
| Delete | O(1) | O(n) | Find + remove from chain |
| Contains Key | O(1) | O(n) | Same as get |
| Resize | O(n) | O(n) | Rehash everything โ happens rarely |
| Iterate all | O(n) | O(n) | Must visit every bucket |
| Operation | Average | Worst | Notes |
|---|---|---|---|
| Add | O(1) | O(n) | Same as map put (key only) |
| Contains | O(1) | O(n) | Hash + check bucket |
| Remove | O(1) | O(n) | Hash + remove from chain |
| Union | O(n+m) | โ | Iterate both sets |
| Intersection | O(min(n,m)) | โ | Iterate smaller, check in larger |
The O(n) worst case isn't just theoretical. If an attacker can control the keys (e.g., HTTP request parameters), they can craft inputs where every key hashes to the same bucket, turning your O(1) map into an O(n) linked list. This is called a hash collision attack (also known as HashDoS). Python and Java randomize hash seeds at startup to prevent this. This is why Python's hash() returns different values across runs.
class HashMap:
def __init__(self, capacity=16):
self._capacity = capacity
self._size = 0
# Each bucket is a list of (key, value) pairs
self._buckets = [[] for _ in range(capacity)]
self._load_threshold = 0.75
def _hash(self, key):
# Use Python's built-in hash, then constrain to our array size
return hash(key) % self._capacity
def put(self, key, value):
if self._size / self._capacity >= self._load_threshold:
self._resize()
idx = self._hash(key)
bucket = self._buckets[idx]
# Check if key already exists โ update if so
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
# New key โ append to chain
bucket.append((key, value))
self._size += 1
def get(self, key, default=None):
idx = self._hash(key)
for k, v in self._buckets[idx]:
if k == key:
return v
return default
def delete(self, key):
idx = self._hash(key)
bucket = self._buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket.pop(i)
self._size -= 1
return True
return False
def contains(self, key):
"""Check if key exists โ O(1) average."""
idx = self._hash(key)
return any(k == key for k, v in self._buckets[idx])
def keys(self):
"""Iterate all keys โ O(n)."""
for bucket in self._buckets:
for k, v in bucket:
yield k
def _resize(self):
# Double capacity and rehash everything
old_buckets = self._buckets
self._capacity *= 2
self._buckets = [[] for _ in range(self._capacity)]
self._size = 0
for bucket in old_buckets:
for key, value in bucket:
self.put(key, value)
# Usage
m = HashMap()
m.put("name", "Alice")
m.put("age", 30)
print(m.get("name")) # "Alice"
m.delete("age")
print(m.get("age")) # None
class HashMap {
constructor(capacity = 16) {
this._capacity = capacity;
this._size = 0;
this._buckets = Array.from({ length: capacity }, () => []);
this._loadThreshold = 0.75;
}
_hash(key) {
// Simple string hash โ multiply-and-add with prime 31
let h = 0;
const str = String(key);
for (let i = 0; i < str.length; i++) {
// The "| 0" forces 32-bit integer to prevent float overflow
h = (h * 31 + str.charCodeAt(i)) | 0;
}
return Math.abs(h) % this._capacity;
}
put(key, value) {
if (this._size / this._capacity >= this._loadThreshold) {
this._resize();
}
const idx = this._hash(key);
const bucket = this._buckets[idx];
// Update existing key if found
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
bucket[i][1] = value;
return;
}
}
bucket.push([key, value]);
this._size++;
}
get(key) {
const idx = this._hash(key);
for (const [k, v] of this._buckets[idx]) {
if (k === key) return v;
}
return undefined;
}
delete(key) {
const idx = this._hash(key);
const bucket = this._buckets[idx];
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === key) {
bucket.splice(i, 1);
this._size--;
return true;
}
}
return false;
}
_resize() {
const oldBuckets = this._buckets;
this._capacity *= 2;
this._buckets = Array.from({ length: this._capacity }, () => []);
this._size = 0;
for (const bucket of oldBuckets) {
for (const [k, v] of bucket) {
this.put(k, v);
}
}
}
get size() { return this._size; }
}
class OpenAddressHashMap:
"""Hash map using open addressing with linear probing.
Instead of chains, collisions are resolved by probing
for the next empty slot in the array. Deletions use a
sentinel value (DELETED) to avoid breaking probe chains.
This is closer to how Python's built-in dict actually works
(though Python uses a more sophisticated probing sequence).
"""
EMPTY = object() # Sentinel for never-used slots
DELETED = object() # Sentinel for deleted slots (tombstone)
def __init__(self, capacity=16):
self._capacity = capacity
self._size = 0
self._keys = [self.EMPTY] * capacity
self._values = [None] * capacity
def _hash(self, key):
return hash(key) % self._capacity
def put(self, key, value):
if self._size >= self._capacity * 0.5: # lower threshold for open addressing
self._resize()
idx = self._hash(key)
while True:
if self._keys[idx] is self.EMPTY or self._keys[idx] is self.DELETED:
self._keys[idx] = key
self._values[idx] = value
self._size += 1
return
if self._keys[idx] == key:
self._values[idx] = value # update existing
return
idx = (idx + 1) % self._capacity # linear probe
def get(self, key, default=None):
idx = self._hash(key)
while self._keys[idx] is not self.EMPTY:
if self._keys[idx] == key:
return self._values[idx]
idx = (idx + 1) % self._capacity
return default
def delete(self, key):
idx = self._hash(key)
while self._keys[idx] is not self.EMPTY:
if self._keys[idx] == key:
# Mark as deleted (tombstone) โ don't use EMPTY
# because that would break probe chains for other keys
self._keys[idx] = self.DELETED
self._values[idx] = None
self._size -= 1
return True
idx = (idx + 1) % self._capacity
return False
def _resize(self):
old_keys, old_values = self._keys, self._values
self._capacity *= 2
self._keys = [self.EMPTY] * self._capacity
self._values = [None] * self._capacity
self._size = 0
for i, k in enumerate(old_keys):
if k is not self.EMPTY and k is not self.DELETED:
self.put(k, old_values[i])
dict[[1,2]] = "x" crashes, but dict[(1,2)] = "x" works.defaultdict or Counter in Python. Writing if key not in dict: dict[key] = 0 everywhere is verbose and error-prone. defaultdict(int) auto-initializes missing keys to 0. Counter(iterable) counts elements in one call. These save time and prevent KeyError bugs.if x in list instead of if x in set. Membership testing in a list is O(n). In a set it's O(1). For any problem where you check "have I seen this before?" more than a few times, convert to a set first.Counting word frequency is one of the most common hash map tasks in production code. Search engines rank pages partly by word frequency. Spam filters count suspicious words. Analytics dashboards count events. Here's a practical implementation:
from collections import Counter
import re
def analyze_text(text):
"""Analyze word frequency in text.
Real-world uses:
- Search engine indexing (TF-IDF scoring)
- Spam detection (count suspicious words)
- Plagiarism detection (compare frequency profiles)
- Natural language processing (bag-of-words model)
"""
# Normalize: lowercase, extract words only
words = re.findall(r'[a-z]+', text.lower())
# Counter is a dict subclass optimized for counting
freq = Counter(words)
# Most common words
print("Top 5 words:")
for word, count in freq.most_common(5):
bar = "โ" * count
print(f" {word:>12}: {bar} ({count})")
# Unique word count
print(f"\nTotal words: {len(words)}")
print(f"Unique words: {len(freq)}")
print(f"Vocabulary richness: {len(freq) / len(words):.2%}")
return freq
# Example
sample = """
The quick brown fox jumps over the lazy dog.
The dog barked at the fox. The fox ran away.
The quick fox was too quick for the lazy dog.
"""
freq = analyze_text(sample)
# Check if two texts are similar (cosine similarity)
def text_similarity(freq1, freq2):
"""Compare two frequency maps using cosine similarity.
Returns 0.0 (completely different) to 1.0 (identical).
This is the basis for document similarity in search engines.
"""
# Only consider words that appear in both texts
common = set(freq1.keys()) & set(freq2.keys())
if not common:
return 0.0
dot_product = sum(freq1[w] * freq2[w] for w in common)
mag1 = sum(v**2 for v in freq1.values()) ** 0.5
mag2 = sum(v**2 for v in freq2.values()) ** 0.5
return dot_product / (mag1 * mag2)
This frequency-counting pattern appears everywhere: log analysis (which error codes appear most?), A/B testing (count conversions per variant), network monitoring (which IPs send the most traffic?), and content moderation (flag posts with unusual word patterns).
Given an array, find two numbers that add to a target. The brute force is O(nยฒ) โ check every pair. The hash map trick: for each number x, check if target - x is already in the map. If yes, you found your pair. If no, add x to the map. One pass. O(n).
This pattern extends to three-sum (sort + two pointers), four-sum, and subarray-sum problems. The core idea โ "compute what I need, check if I've seen it" โ is the single most useful hash map technique.
Build a frequency map, then query it. "Most frequent element", "first unique character", "anagram checking" โ all frequency maps. The pattern is almost always: one pass to build the map, then one pass (or one lookup) to answer the question.
Whenever you catch yourself doing if x in list (O(n)), ask: could this be a set? Converting the list to a set gives O(1) lookups. Problems like "contains duplicate", "intersection of two arrays", "longest consecutive sequence" all rely on this conversion.
Store prefix sums (cumulative sums from the start) in a hash map. For each index, compute the running sum and check if sum - k exists in the map. If it does, there's a subarray summing to k between those two positions. This turns an O(nยฒ) problem into O(n).
Many problems ask you to group items โ anagrams, items by category, nodes by level. The pattern: compute a "group key" for each item (sorted characters for anagrams, tree depth for level-order), then use a hash map from group key to list of items. defaultdict(list) in Python makes this clean.
Problems: Group Anagrams (#49), Group People by Body Size, Accounts Merge.
Combine a sliding window (two pointers defining a range) with a hash map (tracking elements in the current window). The map tells you whether the window is "valid" โ has all required characters, has no duplicates, etc. Problems: Longest Substring Without Repeating Characters (#3), Minimum Window Substring (#76).
Hash map/set problems are everywhere. These cover the main patterns you'll see.
| # | Problem | Difficulty | Pattern |
|---|---|---|---|
| 1 | Two Sum | Easy | Complement lookup |
| 217 | Contains Duplicate | Easy | Hash set membership |
| 242 | Valid Anagram | Easy | Frequency counting |
| 706 | Design HashMap | Easy | Build from scratch |
| 49 | Group Anagrams | Medium | Sorted key grouping |
| 560 | Subarray Sum Equals K | Medium | Prefix sum + map |
| 128 | Longest Consecutive Sequence | Medium | Hash set + smart iteration |
| 146 | LRU Cache | Medium | HashMap + doubly linked list |