Architecture & Invariant Specifications
Our implementation guarantees four structural invariants across every insert, find, erase, and rehash operation:
- Power-of-Two Capacity ($m = 2^r$) with SplitMix64 Finalizer: Every key is mixed through a 64-bit SplitMix64 avalanche permutation so both high and low bits have full entropy, and bucket indexing uses a single bitwise AND
idx = hash & (m - 1). - 1-Based Distance-from-Initial-Bucket (
dib=psl + 1): Instead of storing both abool occupiedflag and auint32_t pslcounter, we store a single integerdib(Distance from Initial Bucket):dib == 0denotes anEMPTYslot, whiledib >= 1denotes an occupied slot with $\text{PSL} = \text{dib} - 1$. - Robin Hood Swap Invariant (
curr_dib > slot.dib): Duringinsert, if an occupied slot has the same key, we update its value in-place; otherwise ifcurr_dib > slot.dib, the incoming entry steals the slot viastd::swapand continues probing with the displaced entry. - Backward-Shift Deletion (
erase): When deleting atidx, we inspectnext = (idx + 1) & mask. Whiletable[next].dib > 1(meaning the occupant atnextis occupied AND displaced from its home bucket, i.e., $\text{PSL} > 0$), we shifttable[next]backward totable[idx]withdib - 1and advanceidx = next. Finally, we cleartable[idx].dib = 0.
| Engine Method | Amortized / Expected Time | Worst-Case Cluster Scan | Auxiliary Memory | Tombstone Overhead |
|---|---|---|---|---|
insert(key, val) |
O(1) | O(log log N) (Random) | O(1) ($O(N)$ on resize) | $0$ Tombstones |
find(key) (Hit & Miss) |
O(1) | Early-exit when dib > slot.dib |
O(1) | $0$ Tombstones |
erase(key) (Backward Shift) |
O(1) | Shifts until dib <= 1 |
O(1) | $0$ Tombstones (Self-healing) |
maxPsl() / Telemetry |
O(m) | Single linear pass | O(1) | Tracks variance compression |
Formal Proof: Correctness of Backward-Shift Deletion Without Tombstones
Suppose slot $i$ is deleted. Let $C = [i+1, i+2, \dots, i+L]$ (modulo $m$) be the maximal contiguous run of slots immediately following $i$ whose occupants satisfy $\text{dib} > 1$ (equivalently $\text{PSL} \ge 1$). For every key $k \in C$, its home bucket $h(k)$ lies at or before $i$ along the cluster, so $k$ originally probed across slot $i$. Shifting every key in $C$ backward by $1$ slot and decrementing its dib by $1$ moves each key $1$ step closer to its home bucket $h(k)$, preserving the Robin Hood ordering invariant $\text{dib}[j+1] \le \text{dib}[j] + 1$ and leaving slot $i + L$ safely EMPTY (dib = 0) because slot $i + L + 1$ either is EMPTY or holds a key at its exact home bucket (dib == 1).
Visualizing End-to-End Insert, Early-Exit Find & Backward-Shift Erase
Compare Robin Hood Hashing (stealing slots from rich keys with lower Probe Sequence Length PSL to crush variance) and Google SwissTable / Rust hashbrown 16-byte H2 metadata SIMD bitmask probing.
Invariant & State
Hardware Reality: Cached Hash Comparison Before Full Key Equality
When keys are strings (std::string) or composite structs, calling slot.key == key dereferences a heap pointer and runs memcmp. By caching the 32-bit or 64-bit hash inside the slot (or comparing slot.dib == dib first, since two keys at the same slot index idx have the exact same home bucket $(idx - (\text{dib}-1)) \bmod m$ if and only if their dib values are equal!), we reject $> 99\%$ of non-matching slots in registers without ever touching the string heap buffer.
Complete Reference Implementation: RobinHoodMap<K, V> in C++20
// C++20: Complete Open-Addressed Robin Hood Hash Table with Tombstone-Free Backward Shift
#include <vector>
#include <cstdint>
#include <optional>
#include <utility>
#include <algorithm>
template <typename K, typename V>
class RobinHoodMap {
struct Slot {
K key{};
V val{};
uint32_t dib = 0; // 0 = EMPTY, >= 1 means PSL = dib - 1
};
std::vector<Slot> table_;
size_t size_ = 0;
static uint64_t hashKey(const K& k) noexcept {
uint64_t x = static_cast<uint64_t>(std::hash<K>{}(k));
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
return x ^ (x >> 31);
}
void rehash(size_t newCap) {
std::vector<Slot> old = std::move(table_);
table_.assign(newCap, Slot{});
size_ = 0;
for (auto& s : old) {
if (s.dib > 0) insert(std::move(s.key), std::move(s.val));
}
}
public:
explicit RobinHoodMap(size_t initialPow2Cap = 16) : table_(initialPow2Cap) {}
size_t size() const noexcept { return size_; }
bool insert(K key, V val) {
// Resize at 85% load factor (17/20)
if ((size_ + 1) * 20 >= table_.size() * 17) {
rehash(table_.size() * 2);
}
const size_t mask = table_.size() - 1;
size_t idx = hashKey(key) & mask;
uint32_t curDib = 1;
while (true) {
Slot& s = table_[idx];
if (s.dib == 0) {
s.key = std::move(key);
s.val = std::move(val);
s.dib = curDib;
++size_;
return true;
}
if (s.dib == curDib && s.key == key) {
s.val = std::move(val); // Update existing key in-place
return false;
}
// Robin Hood Rule: steal slot if incoming key has larger DIB than occupant
if (curDib > s.dib) {
std::swap(key, s.key);
std::swap(val, s.val);
std::swap(curDib, s.dib);
}
idx = (idx + 1) & mask;
++curDib;
}
}
std::optional<V> find(const K& key) const {
const size_t mask = table_.size() - 1;
size_t idx = hashKey(key) & mask;
uint32_t curDib = 1;
while (true) {
const Slot& s = table_[idx];
// Early-exit invariant: if slot is empty OR occupant is richer, key cannot exist
if (s.dib < curDib) return std::nullopt;
if (s.dib == curDib && s.key == key) return s.val;
idx = (idx + 1) & mask;
++curDib;
}
}
bool erase(const K& key) {
const size_t mask = table_.size() - 1;
size_t idx = hashKey(key) & mask;
uint32_t curDib = 1;
while (true) {
Slot& s = table_[idx];
if (s.dib < curDib) return false; // Key not found
if (s.dib == curDib && s.key == key) break; // Located target at idx
idx = (idx + 1) & mask;
++curDib;
}
// Tombstone-free backward shift deletion
size_t next = (idx + 1) & mask;
while (table_[next].dib > 1) {
table_[idx] = std::move(table_[next]);
--table_[idx].dib;
idx = next;
next = (next + 1) & mask;
}
table_[idx] = Slot{};
--size_;
return true;
}
uint32_t maxPsl() const noexcept {
uint32_t maxD = 1;
for (const auto& s : table_) maxD = std::max(maxD, s.dib);
return maxD - 1;
}
};
Failure Modes & Adversarial Pitfalls
- Checking
s.key == keyAfter Swapping Before Checking Duplicate Key: Notice that whens.dib == curDib, both keys share the exact same home bucket (idx - (curDib - 1)). Ifs.dib < curDib, the incoming key could never be a duplicate ofs.key(nor of any key later in the cluster, since later keys have strictly larger home buckets). Thus checkingif (s.dib == curDib && s.key == key)before theif (curDib > s.dib)swap guarantees no duplicate key is ever inserted! - Using Unmixed Identity Hashes with Power-of-Two Masking: Always pass
std::hash<K>through a 64-bit finalizer (SplitMix64) so lower bits used by& (m - 1)reflect all 64 bits of the key.