Drills
Five tasks.
1. A bad hash function
Step 6 said collisions come from too few buckets or a bad hash function. Measure the second one.
Write two alternatives and pour 10,000 items into 16,384 buckets with each, in
place of hashKey:
func lenHash(s string) uint64 { return uint64(len(s)) }
func sumHash(s string) uint64 { /* sum of the bytes */ }
Print how many buckets got used and how long the longest chain is. Explain why
lenHash uses exactly as many buckets as it does — the number is not arbitrary.
Then explain why sumHash is far better than lenHash and still far worse than
FNV-1a. What else does summing destroy that FNV-1a does not? (Hint: "ab" and
"ba".)
2. Delete
Add func (h *HashIndex) Delete(title string, c *metrics.Counter) bool.
Store 1,000 items, delete 500, check that Len() is 500, that the deleted keys
are gone and the rest are still found.
Then answer: should Delete shrink the table when the load factor drops? What
happens if a program adds and deletes around the threshold and your table grows
and shrinks on every operation?
3. Open addressing versus chaining
Step 2 mentioned the other fix for collisions: instead of a chain, look for the
next free bucket (i+1, i+2, …). Implement it separately and compare at equal
load:
buckets load open addressing chaining
16384 0.610 1.783 probes 1.310 comparisons
13000 0.769 2.927 1.381
11000 0.909 5.258 1.446
10500 0.952 12.249 1.486
10100 0.990 25.181 1.497
From 0.610 to 0.990 chaining degrades by a factor of 1.14. Open addressing by a factor of 14.
Explain why. When almost no bucket is free, what is the probe loop looking for, and for how long?
4. What growth costs
Count how many entries were rehashed across every grow call while the table
reached 10,000, and divide by 10,000.
Compare with lesson 2's result: append copies about 4.5 elements per element on
average. Why is it less here when the growth rule — doubling — is the same?
5. Go's map
Rewrite algo index with map[string]Item in place of your HashIndex and
compare using testing.B (not time.Since — 10,000 lookups are too fast for the
clock to show anything).
Then read what Len() returns and explain why you cannot write
for k := range m and expect a stable order — Go is stricter here than your
Keys() is. What did the language designers deliberately do, and why is it
exactly step 8 of this lesson?