A hash table maps keys to values with expected O(1) lookup, insert, and delete. The trick: a hash function turns a key into an integer, which is reduced (modulo the bucket count) into an index, and an entry is stored at that bucket.
The two big design decisions are:
- Hash function — must distribute keys uniformly over
[0, m). - Collision resolution — what to do when two keys land in the same
bucket. Two main approaches:
- Separate chaining — each bucket stores a linked list (or another
small structure) of entries.
- Open addressing — collisions probe to other buckets (linear /
quadratic / double hashing). Used by Go's built-in
map.
The built-in map
m := map[string]int{}
m["a"] = 1
v, ok := m["x"]
delete(m, "a")
for k, v := range m { ... } // iteration order is RANDOMIZED
Properties of Go's map:
- Implemented with open addressing using buckets of 8 entries each.
- Iteration order is intentionally randomized to discourage code that depends on it.
- Reading a non-existent key returns the zero value (and
ok=falsewith the comma-ok form). nilmap can be read but panics on write; always create with{}ormake.- Not safe for concurrent writes; use
sync.Mutexorsync.Map.
You should use map for all real work. The hashmap package in this chapter
is a from-scratch separate-chaining implementation that is only for
learning what the built-in does under the hood.
What we implement
hashmap.HashMap[K comparable, V any] with:
- Separate chaining (singly linked lists per bucket).
- A simple FNV-1a-like hash for
stringkeys; for arbitrarycomparablekeys we fall back tofmt.Sprintand hash that — slow, but correct and educational. - Automatic resize: when the load factor (
n / buckets) exceeds 0.75 we double the bucket count and rehash.
Complexity
| Op | Average | Worst (all collide) |
|---|---|---|
| Get | O(1) | O(n) |
| Put | O(1) | O(n) |
| Delete | O(1) | O(n) |
Run the tests
go test ./part2-dsa/ds/hashmap