Caps ➔ chaining tolerates α≤1 (linear Θ(1+α)); open addressing α<2/3 (probes climb steeply as α→1).
Rehash ➔ cross threshold ⟹ double m, reinsert all n (O(n), but O(1) amortised).
⚙️ Core Implementation
🔹 Hash Function — polynomial / Horner
hash(word, m) and the hash2 non-zero step
def hash(word: str, m: int) -> int: # polynomial rolling hash value, a, b = 0, 31415, 27183 # a, b ideally prime for char in word: value = (ord(char) + a * value) % m a = a * b % (m - 1) # vary the multiplier per position return valuedef hash2(self, key: str) -> int: # double-hashing step, in 1..PRIME (never 0) PRIME = 7 return PRIME - (ord(key[0]) % PRIME)
💡 Common Mistake:Summing chars ignores position ➔ anagrams collide; a multiplier sharing a factor with m (*1024 % 128) keeps only the last char — prime base + prime m, coprime.
🔹 Separate Chaining — a LinkList per cell
chained add / search
def add(self, key, value): h = hash(key) if self.table[h] is None: self.table[h] = LinkList() self.table[h].insert(0, (key, value)) # after checking key absent (update vs add)def search(self, key): chain = self.table[hash(key)] if chain is not None: for k, v in chain: if k == key: return v raise KeyError(key)
💡 Common Mistake:Cost is Θ(1+α) ➔ expected chain length α ⟹ unsuccessful search α, successful 1+α/2; O(1) only while α is bounded.
🔹 Linear Probing — +1 probe, rehash-on-full, correct delete
__linear_probe, __setitem__, __delitem__
def __linear_probe(self, key: str, is_search: bool) -> int: position = self.hash(key) for _ in range(len(self.table)): if self.table[position] is None: if is_search: raise KeyError(key) # off the chain -> absent else: return position # first empty -> insert here elif self.table[position][0] == key: return position # found the key else: position = (position + 1) % len(self.table) # step + wrap raise KeyError(key) # table full -> rehashdef __setitem__(self, key, data): # ADD / UPDATE try: position = self.__linear_probe(key, False) except KeyError: self.__rehash(); self[key] = data # full -> grow + retry else: if self.table[position] is None: self.count += 1 self.table[position] = (key, data)def __delitem__(self, key): # blank then reinsert the cluster pos = self.__linear_probe(key, False) self.table[pos] = None; self.count -= 1 pos = (pos + 1) % len(self.table) while self.table[pos] is not None: item = self.table[pos]; self.table[pos] = None; self.count -= 1 self[str(item[0])] = item[1] pos = (pos + 1) % len(self.table)
💡 Common Mistake:Scan the whole table before rehashing ➔ the add might be an update; in practice rehash much earlier, once α passes the threshold.
🔹 Quadratic & Double Hashing — breaking clusters
__quadratic_probe and __double_hashing
def __quadratic_probe(self, key, is_search): # +i^2 from HOME slot position = self.hash(key); orig = position; step = 1 for _ in range(len(self.table)): if self.table[position] is None: return (_ for _ in ()).throw(KeyError(key)) if is_search else position elif self.table[position][0] == key: return position else: position = (orig + step*step) % len(self.table); step += 1 raise KeyError(key)def __double_hashing(self, key, is_search): # constant key-dependent step position = self.hash(key); step = self.hash2(key) for _ in range(len(self.table)): if self.table[position] is None: if is_search: raise KeyError(key) else: return position elif self.table[position][0] == key: return position else: position = (position + step) % len(self.table) raise KeyError(key)
💡 Common Mistake:Quadratic may never find an empty slot even when one exists; double hashing’s step must be non-zero and coprime to m to visit every slot.
⚖️ Core Decision Matrix
Variant / Strategy
Trigger Condition
Advantage (Pro)
Disadvantage (Con) / Complexity Bound
Cache / Memory Impact
Separate chaining
α may exceed 1
trivial delete (unlink)
Θ(1+α); O(n) if one chain
+1 pointer/node, poor
Linear probing
Low α, cache-critical
excellent locality
primary clustering; O(N) worst
none, in-array
Quadratic probing
Avoid primary clustering
cures primary
secondary remains; may miss slots
none
Double hashing
Near-uniform probing
cures both clusterings
awkward delete; cache-missing jumps
none
When It Flips: dictionary backings — unsorted List (ADT)O(N), Sorted List (ADT)O(logN) search/O(N) add, balanced Binary TreeO(logN) + ordered, Hash TableO(1)∗ but unordered. Use a hash table for big N + no ordering; a BST for range/successor/worst-case guarantees.