diff --git a/wizard-work/beam/robin_hood_hash.py b/wizard-work/beam/robin_hood_hash.py new file mode 100644 index 0000000..9042ea0 --- /dev/null +++ b/wizard-work/beam/robin_hood_hash.py @@ -0,0 +1,163 @@ +"""Robin Hood open-addressing hash table. + +Keys and values can be any hashable/anything. Tombstones are NOT used; deletion +re-hashes the run tail to keep probe distances valid (backward-shift deletion). + +Invariant: for every occupied slot s with key k inserted at home h = hash(k) % n, +probe_distance(s) = (s - h) mod n, and the table is "Robin Hood": slots are +ordered so that no slot has a strictly greater probe distance than an earlier +slot in the same probe chain (Robin Hood property is maintained on insert by +swapping the new entry with the incumbent when the new entry's probe distance +exceeds the incumbent's). + +Complexities: amortized O(1) insert/lookup/delete at load factor < ~0.75. +Resizes by doubling when load factor exceeds 0.7. +""" +from __future__ import annotations + + +class RobinHoodHash: + def __init__(self, capacity: int = 16): + if capacity < 4: + capacity = 4 + # round up to power of two for fast modulo + c = 1 + while c < capacity: + c <<= 1 + self._cap = c + self._keys = [None] * self._cap + self._vals = [None] * self._cap + self._dists = [0] * self._cap # 0 means empty (probe dist >=1 when occupied) + self._size = 0 + + def _home(self, key): + return hash(key) & (self._cap - 1) + + def _resize(self, new_cap): + old_keys = self._keys + old_vals = self._vals + self._cap = new_cap + self._keys = [None] * new_cap + self._vals = [None] * new_cap + self._dists = [0] * new_cap + self._size = 0 + for k, v in zip(old_keys, old_vals): + if k is not None or (k is None and old_keys.index(k) is not None): + pass + # simpler: track occupied via dists of old arrays + # Rebuild from old arrays captured before overwrite + pass + + def __len__(self): + return self._size + + def _insert_raw(self, key, value): + idx = self._home(key) + dist = 1 + while True: + if self._dists[idx] == 0: + self._keys[idx] = key + self._vals[idx] = value + self._dists[idx] = dist + self._size += 1 + return + # Robin Hood: if current entry's probe distance < dist, swap + if self._dists[idx] < dist: + # swap + self._keys[idx], key = key, self._keys[idx] + self._vals[idx], value = value, self._vals[idx] + self._dists[idx], dist = dist, self._dists[idx] + # now continue inserting the displaced entry + idx = (idx + 1) & (self._cap - 1) + dist += 1 + + def __setitem__(self, key, value): + # update existing + idx = self._home(key) + dist = 1 + while self._dists[idx] != 0 and dist <= self._cap: + if dist > self._dists[idx]: + break + if self._keys[idx] == key: + self._vals[idx] = value + return + idx = (idx + 1) & (self._cap - 1) + dist += 1 + # not found -> insert (may resize) + if (self._size + 1) / self._cap > 0.7: + self._grow() + self._insert_raw(key, value) + + def __getitem__(self, key): + idx = self._home(key) + dist = 1 + while self._dists[idx] != 0: + if dist > self._dists[idx]: + raise KeyError(key) + if self._keys[idx] == key: + return self._vals[idx] + idx = (idx + 1) & (self._cap - 1) + dist += 1 + if dist > self._cap: + raise KeyError(key) + raise KeyError(key) + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except KeyError: + return False + + def __delitem__(self, key): + # find slot + idx = self._home(key) + dist = 1 + found = None + while self._dists[idx] != 0: + if dist > self._dists[idx]: + raise KeyError(key) + if self._keys[idx] == key: + found = idx + break + idx = (idx + 1) & (self._cap - 1) + dist += 1 + if dist > self._cap: + break + if found is None: + raise KeyError(key) + # backward-shift deletion + self._keys[found] = None + self._vals[found] = None + self._dists[found] = 0 + self._size -= 1 + # rehash following run + j = (found + 1) & (self._cap - 1) + while self._dists[j] != 0: + k = self._keys[j] + v = self._vals[j] + self._keys[j] = None + self._vals[j] = None + self._dists[j] = 0 + self._size -= 1 + self._insert_raw(k, v) + j = (j + 1) & (self._cap - 1) + + def _grow(self): + old_keys = list(self._keys) + old_vals = list(self._vals) + old_dists = list(self._dists) + self._cap *= 2 + self._keys = [None] * self._cap + self._vals = [None] * self._cap + self._dists = [0] * self._cap + self._size = 0 + for i in range(len(old_keys)): + if old_dists[i] != 0: + self._insert_raw(old_keys[i], old_vals[i]) + + def keys(self): + return [k for k, d in zip(self._keys, self._dists) if d != 0] + + def items(self): + return [(k, v) for k, v, d in zip(self._keys, self._vals, self._dists) if d != 0] diff --git a/wizard-work/beam/test_robin_hood_hash.py b/wizard-work/beam/test_robin_hood_hash.py new file mode 100644 index 0000000..1a43ad2 --- /dev/null +++ b/wizard-work/beam/test_robin_hood_hash.py @@ -0,0 +1,28 @@ +import random, sys +sys.path.insert(0, "/root/.hermes/wizard-outputs/beam") +from robin_hood_hash import RobinHoodHash + +for trial in range(300): + r = RobinHoodHash(8) + d = {} + keys = [random.randint(0, 50) for _ in range(60)] + for k in keys: + r[k] = k*7+1 + d[k] = k*7+1 + for k in keys[::3]: + if k in d: + del r[k]; del d[k] + for k in list(d)[:5]: + r[k] = -1; d[k] = -1 + assert len(r) == len(d), (len(r), len(d), trial) + for k in d: + assert r[k] == d[k], (k, r[k], d[k], trial) + for k in range(60): + assert (k in r) == (k in d), (k, trial) + assert sorted(r.items()) == sorted(d.items()), trial + try: + del r[99999] + raise SystemExit("no raise trial %d" % trial) + except KeyError: + pass +print("ALL 300 RANDOM TRIALS PASS")