🧩 Data Structures & Algorithms · Chapter 3
Hashing and hash maps
A list makes you ask every item 'are you the one?'. A hash map computes where the answer lives and goes straight there — which is why it turns so many O(n²) solutions into O(n).
Study this lesson free in the app →
The basics
A hash map (Python dict, Java HashMap, JavaScript Map) stores key → value pairs and finds any key in about the same time no matter how many it holds. A hash set (set) is the same thing with keys only.
The problem it solves: checking "have I seen this?" or "what's stored under this name?" in a list means scanning — O(n). A hash map instead runs the key through a hash function that turns it into a number, and uses that number to pick a bucket directly: bucket = hash(key) % number_of_buckets. No scanning, so lookups, inserts and deletes are O(1) on average.
The mental model: a cloakroom. You hand in your coat and get ticket 47; to collect it, the attendant walks straight to hook 47. Nobody searches the racks.
Words you must know 5
- Hash function
- turns a key into an integer; the same key must always give the same number.
- Bucket
- one slot of the underlying array.
- Collision
- two different keys landing in the same bucket. Unavoidable; handled by chaining (a small list per bucket) or open addressing (probe for the next free slot).
- Load factor
- entries ÷ buckets. When it gets too high the table resizes (allocates more buckets and re-places every key).
- Complexity
- average O(1) per operation, worst case O(n) if everything collides; O(n) space.
Explained
Find two prices in a list of 100,000 that add up to exactly your gift-card balance.
Compare every pair: about 5 billion checks.
Walk the list once and, for each price, ask a hash set "have I already seen the price that would complete this?": 100,000 checks.
Same answer. The hash map is the difference between minutes and milliseconds.
The cloakroom
A theatre cloakroom has 100 hooks. When you hand in your coat, the attendant looks at your surname, does a quick calculation, and hangs it on hook 47. When you come back, the same calculation sends them straight to hook 47.
That calculation is the hash function. The hooks are buckets.
Sometimes two guests compute to the same hook. The attendant just hangs the second coat on top — a collision, handled by chaining. Finding your coat now means checking two coats on one hook, not walking all 100.
On a sold-out night, hooks hold five coats each and service slows. So the theatre installs a second rack and re-hangs every coat by the new calculation — a resize. Painful once, then fast again.
Precisely
Insert key → value:
- Compute
h = hash(key)— any integer, possibly huge. - Choose a bucket:
i = h % capacity. - If the bucket is empty, store the pair. If not, a collision:
- Chaining (Java's HashMap): append to that bucket's list. - Open addressing (Python's dict): probe other slots in a fixed sequence until a free one turns up.
- If entries ÷ capacity passes a threshold (about 2/3 in CPython, 0.75 in Java), resize: allocate a larger array and re-insert everything, because
h % capacitychanges when capacity does.
Lookup repeats steps 1–2, then compares keys in that bucket with == until it finds a match.
| Operation | Average | Worst | | --- | --- | --- | | insert / lookup / delete | O(1) | O(n) | | iterate all entries | O(n) | O(n) | | space | O(n) | O(n) |
The worst case needs many keys in one bucket. A decent hash function plus resizing makes that vanishingly rare on ordinary data.
Plenty of buckets: every key on its own hook
Five keys, ten buckets — a load factor of 0.5. Watch each key turn into a bucket number. With this much room, keys rarely share a bucket, so a lookup is one hash and one comparison: O(1).
Too few buckets: collisions pile up
Seven keys, three buckets — a load factor of about 2.3. At least one bucket must hold three or more keys, and chains form. Finding a key at the end of a long chain means walking it item by item — a little linear search. This is why real tables resize long before they look like this.
The three patterns you'll use weekly
from collections import Counter, defaultdict
# 1. Count frequencies — O(n) time, O(k) space for k distinct items
Counter("mississippi").most_common(2) # [('i', 4), ('s', 4)]
# 2. Seen-before / dedupe — O(n) time, O(n) space
def first_repeat(items):
seen = set()
for x in items:
if x in seen:
return x
seen.add(x)
return None
# 3. Group by a computed key — O(n · k log k) for n words of length k
def group_anagrams(words):
groups = defaultdict(list)
for w in words:
groups["".join(sorted(w))].append(w) # 'tea' -> 'aet'
return list(groups.values())
group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]Keys must be hashable: use a tuple (row, col), never a list [row, col].
When hashing gets expensive
- Resizing — growing a table with 10 million entries re-places all 10 million. Amortised it's still O(1) per insert, but that one insert can pause for a noticeable moment. Pre-size when you know the count.
- Memory — a hash table keeps spare empty buckets on purpose. A Python dict often uses several times the memory of a plain list of the same items. For hundreds of millions of keys, people reach for compact structures or a database.
- Hash flooding — an attacker who knows your hash function can send keys that all collide, turning every lookup into O(n) and stalling a web server. That's why Python randomises string hashes per process (
hash("abc")changes between runs) and why Java turns very long buckets into small balanced trees. - Across machines — the same
hash(key) % nidea decides which server or partition owns a key. Change n and most keys move; consistent hashing is the fix you'll meet in system design.
Gotchas
- Mutable keys —
d[[1, 2]] = 'a'raisesTypeError: unhashable type: 'list'. Use a tuple. __eq__without__hash__— if two objects are equal they must hash equally, or a set will happily hold "duplicates".- A constant
__hash__—return 1is legal and makes every key collide: O(1) quietly becomes O(n). - Expecting order by key — dicts keep insertion order (Python 3.7+), not sorted order. Need min/max quickly? That's a heap or a sorted structure.
- Calling it O(1) without saying "average" — the worst case is O(n); interviewers listen for the word.
- Iterating while inserting — changing a dict's size during a loop over it raises
RuntimeError.
Words to know
| Hash function | A function that maps a key to an integer; the same key must always produce the same value. |
|---|---|
| Bucket | One slot of the hash table's underlying array, chosen by hash(key) % capacity. |
| Collision | Two different keys mapping to the same bucket. |
| Chaining | Collision handling where each bucket holds a small list of all keys that landed there. |
| Open addressing | Collision handling where a colliding key probes other slots until it finds a free one (used by Python's dict). |
| Load factor | Entries divided by buckets; when it passes a threshold the table resizes. |
| Hashable | Has a hash value that never changes during its lifetime — required for dict keys and set members. |
In two minutes
- A hash map turns a key into a bucket index with hash(key) % buckets, so insert, lookup and delete are O(1) on average. Space is O(n).
- Collisions are normal: chaining keeps a list per bucket, open addressing (what Python's dict does) probes for another slot.
- Keep chains short by keeping the load factor low: the table resizes — roughly doubling and re-placing every key — when it gets too full. Resizes are O(n) but rare, so inserts stay amortised O(1).
- Worst case is O(n) per lookup, when many keys share a bucket — from a bad hash function or deliberately crafted keys.
- Keys must be hashable and must not change: tuples and strings work, lists and dicts raise TypeError.
- Hash maps don't keep keys sorted; finding the min or max key is O(n).
- The big patterns: count frequencies, 'have I seen the complement?' (two-sum), dedupe, and group by a computed key (anagrams).
Test yourself
1. What is the average time complexity of looking up a key in a Python dict?
- O(1)
- O(log n)
- O(n)
- O(n log n)
Show the answer
O(1) — The key's hash points straight at a bucket, so the work doesn't depend on how many entries exist. O(log n) is a balanced tree; O(n) is the rare worst case when everything collides.
2. What is a collision in a hash table?
- Two identical keys inserted twice
- Two different keys mapping to the same bucket
- The table running out of memory
- A key whose hash is negative
Show the answer
Two different keys mapping to the same bucket — Different keys, same bucket. Inserting the same key twice simply overwrites the value — that's not a collision.
3. A hash table has 8 buckets and 6 entries. What is its load factor?
- 8
- 1.33
- 0.75
- 48
Show the answer
0.75 — Load factor = entries ÷ buckets = 6 ÷ 8 = 0.75. That's exactly Java's default resize threshold.
4. You need to count how often each word appears in a 1 GB text file. Best tool?
- A sorted list, searched with binary search for each word
- A list of (word, count) pairs, scanned for each word
- A nested loop comparing each word with every other
- A dict (or Counter) from word to count
Show the answer
A dict (or Counter) from word to count — One pass with O(1) average updates: O(n) total. Scanning a list per word is O(n × distinct words); keeping a sorted list costs O(n) shifting per new word.
5. You want to store grid positions like (row, col) in a set. What should each position be?
- A list [row, col]
- A tuple (row, col)
- A dict {'row': r, 'col': c}
- Any of these
Show the answer
A tuple (row, col) — Set members must be hashable and unchanging. Tuples of numbers are; lists and dicts are mutable and raise TypeError: unhashable type.
6. Two-sum with a dict: walk the array once, checking target - x in seen before inserting x. Complexity?
- O(n) time, O(n) space
- O(n²) time, O(1) space
- O(n log n) time, O(1) space
- O(n) time, O(1) space
Show the answer
O(n) time, O(n) space — n iterations × O(1) average per check = O(n) time, and the dict can hold up to n values. O(n log n)/O(1) is the sort + two-pointers alternative.
There are 9 questions, 4 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →