Robin Hood Hashing: PSL Monotonicity & Tombstone-Free Deletion
For any key $k$ currently stored at index $i$ in a linear-probed table of size $m$, define its Probe Sequence Length (PSL) (also called Displacement Ideal Distance, DIB) as its modular distance from its home bucket:
$$\text{PSL}(k, i) = (i - h(k)) \bmod m$$
During insert(k), we track curr_psl = 0 starting at idx = h(k). At each occupied slot idx, we compare curr_psl against occupant_psl. Whenever curr_psl > occupant_psl, we swap the incoming key with the occupant and continue probing with the displaced occupant. This maintains two powerful invariants:
- Early-Exit Lookup Bound: Because keys along any contiguous cluster are ordered monotonically by their home buckets, if a lookup for
targetreaches a slot wheresearch_psl > occupant_psl,targetcannot exist anywhere further in the table—if it did, it would have stolenoccupant's slot during insertion! Thus unsuccessful searches terminate early without scanning to anEMPTYslot. - Tombstone-Free Backward-Shift Deletion: Instead of leaving a
TOMBSTONEwhen deleting slotidx, Robin Hood inspectsnext = (idx + 1) % m. Whiletable[next]is occupied and hasPSL > 0, it shiftstable[next]backward intotable[idx](decrementing itsPSLby 1) and advancesidx = next. Once it encounters anEMPTYslot or a key sitting at its home bucket (PSL == 0), it markstable[idx] = EMPTY, keeping the table 100% free of tombstones forever.
Cuckoo Hashing: Bipartite Eviction Graphs
In Cuckoo Hashing, two tables $T_1, T_2$ of size $m$ (or one table of size $2m$) use independent hash functions $h_1, h_2$. Every key $x$ resides exclusively at $T_1[h_1(x)]$ or $T_2[h_2(x)]$. Lookups and deletions check only those two slots in parallel—requiring at most 2 cache line reads in the worst case.
To insert $x$, if $T_1[h_1(x)]$ is occupied by $y$, we place $x$ at $T_1[h_1(x)]$ and evict $y$ to its alternate home $T_2[h_2(y)]$, chaining evictions up to $\text{MaxLoop} = lceil 6 \log_{1/(2\alpha)} n \rceil$ steps before triggering a rehash with fresh hash seeds.
| Open Addressing Scheme | Expected Lookup | Worst-Case Lookup (w.h.p.) | Probe Variance | Deletion Strategy & Max Load $\alpha$ |
|---|---|---|---|---|
| Standard Linear Probing | O(1) | O(log N) | $\Theta((1-\alpha)^{-3})$ | Tombstones ($\alpha \le 0.75$) |
| Robin Hood Linear Probing | O(1) | O(log log N) (Random) | $\Theta(\ln((1-\alpha)^{-1}))$ | Backward-Shift, 0 Tombstones ($\alpha \le 0.90$) |
| 2-Way Cuckoo ($d=2, b=1$) | O(1) | Strict 2 Probes | $0$ (1 or 2 probes) | Direct clear ($\alpha < 0.50$) |
| Bucketized Cuckoo ($d=2, b=4$) | O(1) | Strict 2 Cache Lines | $0$ (2 buckets of 4) | Direct clear ($\alpha \le 0.95$) |
Formal Specification: Robin Hood Early-Exit & Cuckoo Graph Thresholds
Robin Hood Ordering Invariant: In any contiguous block of occupied slots, keys appear in non-decreasing order of their home bucket $h(k)$. Consequently, for two adjacent slots $i$ and $i+1 = (i+1) \bmod m$ in the same cluster, $\text{PSL}(i+1) \le \text{PSL}(i) + 1$. During find(q), if at probe step $d = (i - h(q)) \bmod m$ the slot occupant has $\text{PSL}(i) < d$, then $h(\text{occupant}) > h(q)$ along the cluster, proving $q \notin T$.
Cuckoo Graph Theorem (Pagh & Rodler): Define the Cuckoo Graph $G = (V, E)$ with $2m$ vertices (table slots) and $n$ undirected edges $e_x = (h_1(x), h_2(x))$ for each key $x$. An insertion succeeds without rehash iff every connected component of $G$ is unicyclic or a tree (contains at most one cycle, so edges can be oriented with out-degree $\le 1$). At load factor $\alpha = n / (2m) = (1 - \epsilon)/2 < 0.5$, random graph theory (Erdős–Rényi phase transition) guarantees no bicyclic component exists with probability $1 - O(1/n)$.
Visualizing Robin Hood PSL Swapping & Cuckoo Eviction Cycles
Trace Pagh & Rodler Cuckoo Hashing where every key x resides exclusively at T₁[h₁(x)] or T₂[h₂(x)] (guaranteeing worst-case O(1) lookup in 2 parallel cache-line reads) and FKS two-level static perfect hashing.
Invariant & State
Hardware Reality: Memory-Level Parallelism (MLP) in Cuckoo Hashing vs Cache Locality
In Cuckoo Hashing, T1[h1(x)] and T2[h2(x)] have no data dependency between them! An out-of-order CPU core issues both memory loads simultaneously into its Load-Store Queue (Memory-Level Parallelism, MLP), hiding the second DRAM latency completely behind the first. Furthermore, while classic 2-way Cuckoo caps load factor at $\alpha < 0.50$, Bucketized Cuckoo Hashing (storing $b = 4$ slots per bucket so each bucket fits inside one 64-byte cache line, as used in libcuckoo) raises the phase-transition threshold from $49\%$ to **$95\%$ load factor** while still touching at most 2 cache lines per lookup!
Production C++20 Implementation: Cuckoo Hash Table with Unicyclic Eviction Loop
// C++20: 2-Table Cuckoo Hash Map with Worst-Case 2-Probe Lookup & Cycle Detection
#include <vector>
#include <cstdint>
#include <optional>
#include <utility>
template <typename V>
class CuckooHashMap {
struct Entry {
uint64_t key = 0;
V val{};
bool occupied = false;
};
std::vector<Entry> t1_, t2_;
size_t count_ = 0;
uint64_t seed1_ = 0x9e3779b97f4a7c15ULL;
uint64_t seed2_ = 0xbf58476d1ce4e5b9ULL;
static constexpr size_t MAX_KICKS = 64;
size_t h1(uint64_t k) const noexcept {
k ^= seed1_; k *= 0xff51afd7ed558ccdULL;
return (k ^ (k >> 33)) & (t1_.size() - 1);
}
size_t h2(uint64_t k) const noexcept {
k ^= seed2_; k *= 0xc4ceb9fe1a85ec53ULL;
return (k ^ (k >> 33)) & (t2_.size() - 1);
}
void rehash(size_t newHalfCap) {
auto old1 = std::move(t1_);
auto old2 = std::move(t2_);
t1_.assign(newHalfCap, Entry{});
t2_.assign(newHalfCap, Entry{});
seed1_ = seed1_ * 6364136223846793005ULL + 1442695040888963407ULL;
seed2_ = seed2_ * 2862933555777941757ULL + 3037000493ULL;
count_ = 0;
for (auto& e : old1) if (e.occupied) insert(e.key, std::move(e.val));
for (auto& e : old2) if (e.occupied) insert(e.key, std::move(e.val));
}
public:
explicit CuckooHashMap(size_t pow2HalfCap = 16) : t1_(pow2HalfCap), t2_(pow2HalfCap) {}
// Strict O(1) Worst-Case Lookup: Exactly 2 independent array reads
std::optional<V> find(uint64_t key) const noexcept {
const Entry& e1 = t1_[h1(key)];
const Entry& e2 = t2_[h2(key)];
if (e1.occupied && e1.key == key) return e1.val;
if (e2.occupied && e2.key == key) return e2.val;
return std::nullopt;
}
void insert(uint64_t key, V val) {
if (Entry& e1 = t1_[h1(key)]; e1.occupied && e1.key == key) { e1.val = std::move(val); return; }
if (Entry& e2 = t2_[h2(key)]; e2.occupied && e2.key == key) { e2.val = std::move(val); return; }
if ((count_ + 1) * 5 >= (t1_.size() + t2_.size()) * 2) { // Keep load factor < 0.40
rehash(t1_.size() * 2);
}
Entry cur{key, std::move(val), true};
for (size_t kick = 0; kick < MAX_KICKS; ++kick) {
Entry& slot1 = t1_[h1(cur.key)];
if (!slot1.occupied) { slot1 = std::move(cur); ++count_; return; }
std::swap(cur, slot1);
Entry& slot2 = t2_[h2(cur.key)];
if (!slot2.occupied) { slot2 = std::move(cur); ++count_; return; }
std::swap(cur, slot2);
}
// Bicyclic component detected: rehash with new seeds and re-insert displaced cur
rehash(t1_.size() * 2);
insert(cur.key, std::move(cur.val));
}
};
Failure Modes & Adversarial Pitfalls
- Shifting
PSL == 0Keys During Robin Hood Backward-Shift Deletion: Iferaseshifts an adjacent key whosePSL == 0backward by 1 slot, that key moves before its home bucket $h(k)$ (giving it a wrappedPSL = m - 1) and renders it permanently unreachable byfind(k)! Backward shift must stop immediately whentable[next].psl == 0. - Single-Table Cuckoo Self-Loop ($h_1(x) == h_2(x)$): If both hash functions map into the same shared array of size $m$ and $h_1(x) = h_2(x)$ for some key $x$, a second key $y$ with $h_1(y) = h_2(y) = h_1(x)$ forms an immediate 2-element bicyclic crash. Either use two disjoint tables $T_1, T_2$ or force $h_2(x) \ne h_1(x)$.