beam: Robin Hood open-addressing hash table with randomized correctness tests

This commit is contained in:
beam committed 2026-10-26 08:28:10 +00:00
1 parent dfea35ef56
commit 8ca7e8cdc0
2 files changed
+191

No files matched your search

+163
View File
@@ -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]
+28
View File
@@ -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")