Recursion and Hashing
Hashing and hash tables
Turning a key into an address for instant lookup
How a dictionary lookup that could scan a million keys instead finds any one of them in a single step - most of the time.
An array gives you instant access if you already know the index. But your keys are usually names, words, URLs - not integers 0…n. A is the bridge: it turns any key into an array index, so table["owl"] becomes a plain array fetch. The catch is that different keys can map to the same index - a - and the entire craft of hash tables is keeping those collisions rare enough that lookups stay fast.
The coat-check analogy
A key becomes an index
A hash function chews on the raw bytes of a key and spits out an integer. To land inside a table of buckets, we fold that integer down with the modulo operator. That single line is the whole trick:
From key to bucket
where is the raw hash (some big integer) and is the number of buckets. Because the result is a genuine array index, reading or writing that bucket is the same address arithmetic you saw for arrays - no scan involved. A good hash function has three jobs: be deterministic (same key -> same index, every time), fast to compute, and uniform - scatter keys evenly so no bucket hogs them. A tiny change in the key ideally lands in a totally different bucket.
# Turn any string key into a bucket index.
def bucket_of(key, m):
h = 0
for ch in key: # 'owl' -> 111 + 119 + 108
h += ord(ch) # h = 338 (the raw hash)
return h % m # 338 % 8 = 2 -> bucket 2
# Real hashers MIX bits instead of just adding, e.g.
# h = h * 31 + ord(ch)
# so that 'listen' and 'silent' don't collide the way a plain sum does.A plain sum is a bad hash - on purpose
listen / silent, ant / tan) share the same sum, so they always collide. Production hashes multiply-and-mix the bits (Java's hashCode, Python's SipHash) so that even "cat" and "cau" fly to unrelated buckets. Same idea, far better spread.Collisions: when two keys want the same box
Uniform or not, collisions are inevitable - with more keys than buckets they're guaranteed. Two strategies dominate. (what the sandbox shows) hangs a little linked list off each bucket; colliding keys just join the chain. keeps one key per bucket and, on a clash, probes onward - linear probing simply tries the next bucket, then the next, until it finds an empty slot. Chaining is simpler and degrades gracefully; open addressing is cache-friendlier because everything lives in the one contiguous array.
Build a hash table by hand
Type a key (or click a preset animal) and watch the hash function compute its bucket live. Keys that collide stack up into visible chains. Keep inserting - or drag the rehash threshold - and when the load factor crosses it, the whole table doubles and every key is re-bucketed into the bigger array before your eyes.
Hash-table sandbox type - insert - rehash
h(key) mod m pick its bucket.Empty table with 8 buckets. Insert keys and watch collisions form chains.
Load factor decides the speed
The load factor is simply how full the table is:
Under uniform hashing the expected length of any chain is exactly , so an average lookup costs - the to compute the hash, the to walk that bucket's chain. Keep below a constant (Java uses ) and both terms are constant, giving amortized . Let drift upward and chains lengthen, so we rehash: allocate a table roughly twice as big and reinsert every key. That's an jolt, but like the dynamic-array doubling you already met, doublings are rare enough that the cost amortizes away.
Sets, maps, and the O(n) cliff
The same machinery powers two everyday structures. A stores only keys and answers "is present?" - Python's set, Java's HashSet. A stores a value alongside each key and answers "what is mapped to?" - Python's dict, Java's HashMap. A set is just a map whose values you ignore; both hash the key to find the bucket in the same way.
But that cheerful is an average, not a guarantee. If a bad hash (or an adversary picking keys on purpose) funnels every key into one bucket, the table collapses into a single long chain and every lookup scans it - , no better than an unsorted list. Good hashing and a bounded load factor are what keep you off that cliff.
The aha
Check yourself
With a good hash function and the load factor held low, an average hash-table lookup costs:
A hash table degrades to O(n) lookup precisely when:
Recall: what does the load factor α measure, and why do we rehash when it climbs?
Tie the load factor to chain length and lookup cost. Try to state it, then check.
Lock it in
- A hash function manufactures an index from a key - - so a lookup is O(1) address arithmetic, never a scan.
- A good hash is deterministic, fast, and uniform; collisions are inevitable and are absorbed by chaining or open addressing.
- Average cost is for load factor ; keep bounded by rehashing (double and reinsert) for amortized O(1).
- The O(1) is an average, not a guarantee: funnel every key into one bucket and the table collapses to an O(n) linear scan.
Primary source
Ask your teacher