Hashing#
Hash tables map keys to values using a hash function to index into an array. Average-case O(1) lookup, insert, and delete make them the default tool for frequency counting, caching, and deduplication in interviews.
When to use hashing#
| Scenario | Structure |
|---|---|
| Count occurrences | dict or Counter |
| Check if seen before | set |
| Group items by key | defaultdict(list) |
| O(1) lookup by arbitrary key | dict |
| Two-sum style complement lookup | dict while scanning |
| Graph adjacency (sparse) | dict of lists |
Avoid hashing when: you need sorted order (use BST or sort), range queries on keys (use segment tree / sorted structure), or prefix matching on strings (use trie).
Complexity#
| Operation | Average | Worst case |
|---|---|---|
| Insert | O(1) | O(n) |
| Search | O(1) | O(n) |
| Delete | O(1) | O(n) |
| Space | O(n) | O(n) |
Worst case occurs when many keys collide into the same bucket (adversarial inputs or poor hash function). Python's dict and set use open addressing with a high-quality hash and resize when load factor grows — worst case is rare in practice.
How hashing works#
- Hash function
h(key)→ integer index in[0, table_size). - Store
(key, value)at that index (or in a chain at that index). - Lookup: compute
h(key), probe bucket, compare keys (must handle collisions).
Load factor α = n / table_size. When α is too high, resize and rehash (Python dicts do this automatically).
Python built-ins (use these in interviews)#
Dictionary#
freq: dict[str, int] = {}
for word in words:
freq[word] = freq.get(word, 0) + 1
# membership is by key
if target in index_map:
...
Set#
defaultdict and Counter#
from collections import defaultdict, Counter
graph = defaultdict(list)
graph[u].append(v)
counts = Counter(nums) # element → frequency
most_common = counts.most_common(3)
Time: same as dict — O(1) average per operation.
Space: O(n) for n stored keys.
When to pick which#
| Type | Keys | Values | Duplicates |
|---|---|---|---|
dict |
unique | any | keys unique |
set |
unique | none | no |
defaultdict |
unique | default factory | keys unique |
Counter |
unique | integer counts | multiset semantics |
Hash collisions#
Two keys can map to the same index. Resolution strategies:
| Strategy | Idea | Trade-off |
|---|---|---|
| Chaining | Each bucket is a list of entries | Simple; extra pointer overhead |
| Open addressing | Probe to next empty slot (linear, quadratic, double hashing) | Cache-friendly; clustering possible |
Python 3.7+ dict uses open addressing (not chaining). Understanding chaining below still helps whiteboard questions.
Implementation: separate chaining#
Time: O(1) average if load factor bounded and hash spreads keys; O(n) worst if all keys collide.
Space: O(n + table_size).
Implementation: open addressing (linear probing)#
Time: O(1) average with low load factor; degrades as table fills (clustering).
Space: O(table_size).
Hash function properties (conceptual)#
A good hash function should:
- Spread keys uniformly across buckets
- Be deterministic (same key → same index)
- Be O(1) to compute
Built-in hash() in Python is salted per process (not stable across runs) — fine for dicts, not for serializing hash values.
Hashing vs other structures#
| Need | Use |
|---|---|
| Exact key lookup, no order | Hash map / set |
| Sorted keys, predecessor/successor | BST or sorted array + binary search |
| String prefixes | Trie |
| Priority by value | Heap |
Related pages#
- Overview — structure selection guide
- Python Built-in Data Structures —
dict,set,Counter - Trie — string-keyed alternative