A hash map is the machinery under dict and set: it turns a key into a slot number so lookups avoid scanning the whole collection.
Core mechanism
- Hash function: takes a key, returns a fixed-size number (the hash). Good ones are deterministic (same key, same number) and spread keys out.
- Path from key to box: key ->
hash()-> big number ->% num_slots-> slot index (always0toslots - 1). - Slot: a numbered box; each slot holds a bucket (a small list of pairs).
- Python’s
hash:hash(42)is42; string hashes are randomized between separate runs, stable within one run.
"Alice" -> hash() -> 6182644... -> % 8 -> slot 4
Collisions
- Collision: two different keys map to the same slot. Guaranteed once keys outnumber slots; normal, not an error, and never affect correctness (only speed).
- Separate chaining: each slot’s bucket holds all colliding pairs; lookup scans that short list.
- Open addressing: no side-lists; probe for the next free slot by a fixed rule. Python’s real
dictuses a form of this.
Load factor and resizing
- Load factor = entries / slots. Low = short buckets, wasted memory; high = long buckets, slower lookups.
- Resize: when load factor crosses a threshold (Python’s
dict~ two-thirds full), allocate a bigger array (~double) and rehash every entry, because the slot came fromhash(key) % num_slotsandnum_slotschanged. - A single resize is O(n) but rare and doubles capacity, so insert stays amortized O(1).
Big-O of every operation
| Operation | Average | Worst |
|---|---|---|
Lookup (d[key], key in d) | O(1) | O(n) |
Insert (d[key] = v) | O(1) amortized | O(n) |
Delete (del d[key]) | O(1) | O(n) |
| Update existing key | O(1) | O(n) |
| Resize / rehash | O(n) | O(n) |
| Iterate all items | O(n) | O(n) |
- Average O(1): hash, jump to slot, scan a near-constant bucket. Worst O(n): every key collides into one bucket.
- Space is O(n).
Why keys must be hashable and immutable
- A key must be hashable; in Python that means immutable. Numbers, strings, tuples of immutables work; lists, dicts, sets do not.
- Reason: the slot comes from the hash. A mutable key’s hash could change, moving its correct slot while the pair stays in the old slot, so the value gets “lost.”
- Values have no restriction; only the key feeds the hash.
Gotchas
- Convert a mutable key first:
d[tuple(my_list)] = v. - Don’t save a hash value or rely on hash-driven order across runs.
dictpreserves insertion order (guaranteed since 3.7);setdoes not.- O(1) is an average, not “instant” or “always”; state it as “O(1) average, O(n) worst.”
setis a hash map with values removed: membership is O(1) average, elements must be hashable.