Three parts in pseudocode
Three parts: the hash function, collision handling, and growth.
The hash function
FNV-1a — one of the simplest good ones. One xor and one multiply per byte:
hashKey(s):
h ← 14695981039346656037 // offset basis
for each byte b of s:
h ← h XOR b
h ← h * 1099511628211 // prime
return h
The bucket number is hashKey(title) % len(buckets).
It works on BYTES, and for a hash table that is correct. Two identical strings have identical bytes whatever alphabet they are written in; two different ones almost certainly differ. The function does not need to know what a letter is.
In step 7 you will meet a structure for which bytes are not enough — and why.
Collisions: chains
Two different keys can land in the same bucket. The simplest fix: each bucket holds a chain, and a lookup walks it.
Get(title):
i ← hashKey(title) % len(buckets)
for e from buckets[i] along the chain:
c.Hit()
if e.key == title: return e.item, true
return empty, false
The comparison count is the chain length. That is why the counter matters so much here: it measures precisely the quantity that decides whether the O(1) is real.
An equal key is not a collision
This is easy to conflate, so plainly:
- a collision is different keys landing in the same bucket. Unavoidable, handled by the chain;
- an equal key is the same title stored twice. That is not a collision;
Putmust replace the existing entry.
In lesson 1 you wrote gen -dup-rate. It produces equal keys, not
collisions — and step 6 shows what it actually does to the index.
Growth
As items accumulate without more buckets, chains lengthen and the O(1) becomes a lie.
The load factor is items ÷ buckets. When it reaches 1.0, the bucket count doubles and everything is rehashed:
grow():
old ← buckets
buckets ← a new array, twice the size
for every entry in old: insert it again using hashKey % the new size
That is an O(n) operation. But it happens rarely — exactly like lesson 2's
append, and for the same reason. Amortization is the same idea here.