Why Robin Hood Hashing Matters
A cache index may hold many keys close to their preferred slots while a few unlucky keys travel through long collision clusters. Those outliers make lookup costs uneven. Robin Hood Hashing changes who keeps a contested slot: the incoming key takes it if that key has already traveled farther than the resident.
This lesson uses Robin Hood linear probing. Keys advance one slot at a time, wrapping around the array. Each slot stores the key's distance from its home bucket, allowing insertion to compare displacement and an unsuccessful lookup to stop before reaching an empty slot in some cases. Backward-shift deletion preserves that lookup rule without tombstones.
You will trace a set of collisions, implement an exact integer set in Python, C++, and Java, and verify insertion, early termination, deletion, and growth. The sample also shows what “fairness” means: the displacement distribution improves, while the average successful-search cost in this particular comparison stays the same.
Robin Hood Hashing gives a contested slot to the key with the larger current probe distance, then carries the displaced resident onward. This redistributes collision costs and supplies an early-stop rule for lookup. Correctness depends on accurate distance metadata and deletion repair; it does not imply constant worst-case lookup or eliminate clustering.
Core Concepts and Terminology
The input is a sequence of set operations on comparable keys. The runnable programs use signed $32$-bit integers. The set stores each member exactly once in an array of capacity $m$, with a separate representation for an empty slot. Zero and negative integers are ordinary keys, not empty markers.
A seeded hash function maps key $x$ to its home bucket $h(x)\in[0,m)$. Its linear probe sequence is
$$h(x),\ (h(x)+1)\bmod m,\ (h(x)+2)\bmod m,\ldots.$$
If $x$ occupies slot $j$, its distance from initial bucket, or DIB, is
$$\operatorname{DIB}(x,j)=(j-h(x)+m)\bmod m.$$
Distance counts moves, not probes: a key at home has DIB $0$ but needs one slot inspection for a successful lookup. The sample displays an occupied slot as key:dib. Empty slots have no meaningful DIB; the implementation resets their metadata to zero for clarity.
Let $n$ be the number of members and $\alpha=n/m$ the load factor. The code grows before a new insertion would exceed $3/4$ load, leaving empty space for a terminating probe sequence. This is a demonstration policy, not a universal optimum.
| Term | Meaning and role |
|---|---|
| Home bucket | $h(x)$, the first slot checked for key $x$; collisions occur when keys share a home. |
| DIB | Circular distance from home to the occupied slot; a key with home $0$ in slot $2$ has DIB $2$. |
| Incoming distance | The pending key's distance at the slot currently being inspected; it changes when a resident is displaced. |
| Steal | If incoming distance is larger than resident DIB, swap both key and distance metadata. |
| Tie rule | Equal distances do not swap in this implementation; specifying ties makes layouts reproducible. |
| Cluster | A contiguous run of occupied slots, possibly wrapping around the array; Robin Hood placement does not remove such runs. |
| Early termination | Stop an unsuccessful lookup when resident DIB is smaller than the query's current distance. |
| Backward shift | Move a displaced successor into a deletion hole and decrement its DIB by one. |
| Rehash during growth | Reinsert every member because changing capacity changes home buckets and distances, even if the hash salt stays fixed. |
| Load factor | $\alpha=n/m$; the sample has $5/8$ before deletion and $4/8$ afterward. |
An important invariant is not that DIBs must be globally nondecreasing inside a cluster. For example, with homes $0,0,2$, a valid three-key layout can have DIBs $[0,1,0]$ in consecutive occupied slots. The drop simply starts a key at its own home.
The useful lookup invariant is more precise: for every stored key $x$ with DIB $d$, each earlier slot on its probe path at offset $t\lt d$ is occupied by a resident whose DIB is at least $t$. A search for $x$ therefore cannot encounter an empty slot or a smaller resident DIB before reaching $x$. This path-based statement supports early termination even when adjacent DIBs decrease.
How Robin Hood Hashing Works, Step by Step
Lookup. Start at the query key's home with query distance $d=0$. At each slot, return absent if it is empty; return present if its full key matches; otherwise return absent if resident DIB is smaller than $d$. If none applies, advance one slot circularly and increment $d$. The loop also has a capacity bound as a defensive guard.
Insertion. First look for the key so that a duplicate cannot be inserted or trigger unnecessary growth. If the new load would exceed $0.75$, build a doubled table and reinsert the old members. Then carry the new key with distance zero from its home:
- If the current slot is empty, store the carried key and its distance, then finish.
- If resident DIB is smaller than incoming distance, exchange both the keys and their distances. Count a steal.
- Advance one slot and increment the distance of whichever key is now being carried.
The distance exchange in step $2$ is essential. An evicted key takes its own old DIB onward, not the larger distance of the key that replaced it. Its distance increases by one when it moves to the next slot.
flowchart TD
A["Insert key"] --> B{"Already present?"}
B -->|Yes| C["Return false"]
B -->|No| D{"New load exceeds 0.75?"}
D -->|Yes| E["Grow and reinsert all old keys"]
D -->|No| F["Carry new key from home at distance 0"]
E --> F
F --> G{"Current slot empty?"}
G -->|Yes| H["Store key and distance; increase size"]
G -->|No| I{"Incoming distance greater than resident DIB?"}
I -->|Yes| J["Swap keys and distances"]
I -->|No| K["Advance circularly; increase carried distance"]
J --> K
K --> G
Deletion. Find the key using the same lookup rule. Treat its slot as a hole, then inspect the next slot. While that successor is occupied and has positive DIB, move it into the hole, reduce its DIB by one, and move the hole forward. Stop at an empty slot or a resident with DIB zero, then clear the final hole and decrease the size.
A resident at distance zero must stay at its home: shifting it backward would move it outside its permitted probe path. An empty slot also ends the repair. These stopping conditions keep searches valid and avoid accumulating tombstones. All operations here are single-threaded; during displacement or shifting, transient states are not suitable for unsynchronized readers.
Worked Example with a Full Trace
Use capacity $m=8$, seed $6$, and insertion order $[0,11,2,13,15]$. The generator supplies salt $100842$. With the code's integer mixer, the home buckets are
$$h(0)=0,\quad h(11)=1,\quad h(2)=2,\quad h(13)=0,\quad h(15)=0.$$
The first three keys go directly into their homes. The next two insertions create the interesting changes. The table records every insertion-slot inspection; duplicate-check lookup probes are separate and are not included in the printed insertion counter.
| Probe | Carried key | Slot | Incoming DIB | Resident before inspection | Action |
|---|---|---|---|---|---|
| $1$ | $0$ | $0$ | $0$ | Empty | Store $0:0$. |
| $2$ | $11$ | $1$ | $0$ | Empty | Store $11:0$. |
| $3$ | $2$ | $2$ | $0$ | Empty | Store $2:0$. |
| $4$ | $13$ | $0$ | $0$ | $0:0$ | Tie: keep resident; advance with $13$ at distance $1$. |
| $5$ | $13$ | $1$ | $1$ | $11:0$ | Steal: store $13:1$; carry $11$ onward at distance $1$. |
| $6$ | $11$ | $2$ | $1$ | $2:0$ | Steal: store $11:1$; carry $2$ onward at distance $1$. |
| $7$ | $2$ | $3$ | $1$ | Empty | Store $2:1$. |
| $8$ | $15$ | $0$ | $0$ | $0:0$ | Tie: advance with distance $1$. |
| $9$ | $15$ | $1$ | $1$ | $13:1$ | Tie: advance with distance $2$. |
| $10$ | $15$ | $2$ | $2$ | $11:1$ | Steal: store $15:2$; carry $11$ onward at distance $2$. |
| $11$ | $11$ | $3$ | $2$ | $2:1$ | Steal: store $11:2$; carry $2$ onward at distance $2$. |
| $12$ | $2$ | $4$ | $2$ | Empty | Store $2:2$. |
After all five insertions, slots $0$ through $4$ contain $[0:0,13:1,15:2,11:2,2:2]$ and the other slots are empty. There were $12$ insertion-slot inspections and $4$ steals. The load is $5/8$, so no growth was needed.
Ordinary first-come linear probing with the same homes and insertion order would place the keys as $[0:0,11:0,2:0,13:3,15:4]$. Its DIB values are $[0,0,0,3,4]$, compared with Robin Hood's $[0,1,2,2,2]$. Both sums are $7$, both mean DIBs are $7/5=1.4$, and both mean successful-search probe counts are $2.4$. Population DIB variance drops from $3.04$ to $0.64$, and maximum DIB drops from $4$ to $2$ on this example. Better balance is distinct from a lower mean.
Now insert $13$ again. Lookup finds it, so insertion returns false without changing size, layout, or statistics. Delete $13$ from slot $1$ and repair the hole:
| Repair step | Move | Distance change | New hole |
|---|---|---|---|
| $1$ | Key $15$ from slot $2$ into slot $1$ | $2\to1$ | Slot $2$ |
| $2$ | Key $11$ from slot $3$ into slot $2$ | $2\to1$ | Slot $3$ |
| $3$ | Key $2$ from slot $4$ into slot $3$ | $2\to1$ | Slot $4$ |
| Stop | Slot $5$ is empty; clear slot $4$ | Empty-slot DIB becomes $0$ | None |
The final layout is $[0:0,15:1,11:1,2:1,\mathrm{empty},\mathrm{empty},\mathrm{empty},\mathrm{empty}]$, with size $4$ and $3$ backward shifts. Keys $0,15,11,2$ remain reachable, and $13$ is absent.
Trace It by Hand
At probe $10$, incoming key $15$ has distance $2$, while resident $11$ has DIB $1$. Predict the carried state after the steal and one advance. Slot $2$ receives $15:2$, and the pending key becomes $11$ with its own old distance $1$, then advances to slot $3$ at distance $2$. Keeping the incoming distance after the swap would give the wrong metadata.
For an early-stop exercise, build a fresh table with insertion order $[0,13,2]$. Its occupied DIB sequence is $[0,1,0]$. Searching for absent key $15$, whose home is $0$, inspects offsets $0,1,2$ and stops at slot $2$ because resident DIB $0\lt2$. The DIB drop is valid; it certifies absence for this query rather than invalidating the table.
Implementation in Python, C++, and Java
Each panel is a complete set implementation with add, contains, and remove. A new insertion returns true; a duplicate returns false. Removing an absent key returns false. The code maintains explicit occupancy so every signed $32$-bit key, including zero and the minimum negative value, can be represented.
For cross-language replay, a specified generator produces one salt at construction: $s\leftarrow16807s\bmod2147483647$, with seed $1\le s\le2147483646$. The same xor-shift mixer and wrapping multiplications modulo $2^{32}$ are then used in all three languages. Python masks the products, C++ uses uint32_t, and Java uses wrapping int arithmetic with unsigned right shifts and an unsigned conversion before the capacity reduction.
The salt remains fixed during growth; reinsertion recalculates every home modulo the new capacity. There is no per-insertion random choice. This seeded mixer is for transparent replay, not a proof that the hash homes satisfy an ideal random model or resist adversarial input.
Capacity must be at least $4$ and no greater than the configured maximum, which is capped at $1048576$ for this example. A new key that would exceed $3/4$ load requests doubled capacity. If that exceeds the maximum, insertion raises a capacity-limit error before changing the table or counters. Duplicates are checked before that guard, so a duplicate still returns false at the load ceiling.
insert_probes counts slot inspections by placement routines, including reinsertion during growth. It excludes the preliminary duplicate lookup. steals counts key-and-DIB exchanges, shifts counts deletion repair moves, and grows counts successful capacity doublings. Lookup probes are not included in these counters. The lower-level placement helper assumes a genuinely new key and at least one empty slot; public operations maintain those preconditions.
Use Python 3, C++14 or later, or Java 17 or later. Compile C++ with g++ -std=c++14 -O2 main.cpp -o robin; compile and run Java with javac Main.java and java Main. All three programs print these six lines:
before = [0:0, 13:1, 15:2, 11:2, 2:2, _, _, _]after erase 13 = [0:0, 15:1, 11:1, 2:1, _, _, _, _]size=4, capacity=8insert probes=12, steals=4, shifts=3, grows=0hash draws = 1checks = PASS
The output before deletion makes the insertion statistics independently checkable, and the final check confirms duplicate detection, deletion, remaining members, and an absent key. A stronger test should scan every stored key and DIB, compare operations with an independent reference set, and include wrapped clusters and growth.
Complexity, Correctness, and Invariants
For fixed-size keys and constant-cost hashing, successful or unsuccessful lookup can take constant time in the best case. At a fixed load bounded below $1$, sufficiently well-distributed hash homes give constant expected probe costs; the constants depend on load. The same expectation supports amortized constant insertion with geometric growth. These are distributional claims, not worst-case guarantees for the fixed public mixer.
At fixed capacity, a search or insertion may traverse a cluster containing all $n$ members, and deletion may shift almost all of it. Their worst-case time is $O(n+1)$, with matching linear examples for nontrivial $n$. Each ordinary operation uses $O(1)$ auxiliary state. The table and its metadata use $\Theta(m)$ storage and are not automatically shrunk after deletion.
Growth allocates a new array, scans the old capacity, and reinserts the old keys. Its expected cost is $O(m+n)$ under suitable hashing, but the actual reinsertion routine can take $O(n^2)$ time on an adversarial collision pattern. A growth operation therefore has deterministic upper bound $O(m+n^2)$ in this code, with $O(m)$ additional table storage during doubling. When growth is triggered by the load ceiling, $m$ and $n$ are proportional. Calling every resize worst-case linear would ignore the reinsertion cost.
| Scheme or operation | Expected behavior with suitable hashing and controlled load | Worst-case behavior | Main tradeoff |
|---|---|---|---|
| Robin Hood lookup | $O(1)$ | $O(n+1)$ | DIB-based early termination; still subject to long clusters. |
| Robin Hood insertion without growth | $O(1)$ | $O(n+1)$ | May exchange several residents while traversing a cluster. |
| Backward-shift deletion | $O(1)$ under the usual random-home, fixed-load model | $O(n+1)$ | Repairs the cluster without tombstones; moves surviving entries. |
| Geometrically growing implementation | Expected amortized $O(1)$ insertion | A growth insertion can cost $O(m+n^2)$ | Recomputes homes and DIBs; temporarily stores old and new arrays. |
| Ordinary linear probing | Expected $O(1)$ at load bounded below $1$ | $O(n+1)$ per fixed-capacity operation | Simple local probes, but unequal displacements and no Robin Hood early-stop rule. |
| Basic Cuckoo Hashing | Worst-case constant candidate checks for lookup | An insertion can require a global rebuild | Moves insertion complexity to a two-choice placement problem. |
| Balanced search tree | $O(\log(n+1))$ | $O(\log(n+1))$ | Ordered operations and deterministic bounds instead of array-local probing. |
Correctness has three parts. First, the metadata of every resident must equal its circular distance from home. Second, the resident keys must be exactly the set's members, with no duplicates. Third, the lookup-path invariant must hold: before a stored key at distance $d$, each earlier offset $t$ encounters a resident with DIB at least $t$.
Insertion preserves these properties because a pending key passes a resident only when resident DIB is at least its own current distance. Otherwise it takes that slot, and the displaced key carries its own distance forward. Raising a resident DIB by a steal cannot make an existing search terminate earlier at that slot. When an empty slot is reached, all members are resident again and the size increases once.
Every key's DIB is its actual circular distance from home, and every earlier offset on its search path is occupied by a resident whose DIB is at least that offset. Therefore an empty slot or a resident with smaller DIB proves absence. Swapping key and distance together, then repairing deletions by backward shifts, preserves this rule; global nondecreasing DIB order is unnecessary.
Backward shifting moves a successor one step closer to its own home, so its DIB must decrease by exactly one. To see why it remains safe for other searches, consider a query that would have passed the hole before reaching a later member. At the successor slot its old query distance was one greater; the lookup invariant required the successor's DIB to be at least that distance. Moving the successor backward and subtracting one preserves the required inequality at the hole.
If the successor is at home, its DIB is zero. No remaining member can require a search path to cross the hole and that zero-DIB successor: such a path would already violate the lookup invariant at the successor. Repair may therefore stop there. An empty successor similarly marks the end of any continuing probe path. These arguments also apply when the hole advances from slot $m-1$ to slot $0$.
All scans are bounded. The public load policy guarantees an empty slot before ordinary placement; linear probing advances the slot on every iteration, so placement reaches an empty slot within one circuit. Lookup additionally stops on an early certificate, and deletion advances its hole until the next empty slot or zero-DIB resident.
Variants, Pitfalls, and Optimizations
Simply clearing a deleted slot is unsafe. In the sample, clearing slot $1$ after deleting $13$ would make a search for $15$ stop there as absent, even though $15$ remained at slot $2$. The three backward shifts move the surviving keys closer to home and leave the final hole at a safe stopping boundary.
Other implementation decisions have distinct consequences:
- Move metadata with the key. A steal exchanges the pending and resident DIBs as well as their keys. Increment only the distance of the key being carried to the next slot. A deletion shift decrements the moved key's DIB, and a resize recomputes it from scratch.
- Use the correct early-stop inequality. Stop when resident DIB is strictly smaller than query distance. Equality is not an absence certificate: a later colliding key can still be found after passing a tied resident. Keep the full-key equality check as well.
- Handle wraparound in every operation. Advance by $(j+1)\bmod m$, and interpret DIB circularly. A cluster beginning near the end of the array can continue at its beginning; deletion must repair across the same boundary.
- Do not assume sorted DIBs. The valid sequence $[0,1,0]$ is a counterexample. Test the lookup-path invariant and exact metadata instead of rejecting every decrease.
- Do not transplant tombstones casually. A tombstone design needs its own search and insertion rules. Treating a tombstone as empty loses keys, while reusing its stale DIB with the early-stop rule can also be wrong. This implementation uses backward shifting throughout.
- Control load and displacement ranges. Higher utilization increases cluster lengths even with a good hash. A compact DIB field can overflow on a long cluster; grow, widen it, or provide an explicit fallback rather than wrapping metadata silently. The sample uses ordinary integer distances at a bounded capacity.
- Generalize records carefully. A map must move keys and values together. Insertion, deletion, and growth can relocate elements, so pointers and iterators into a flat table may be invalidated. Concurrent operations require an appropriate relocation protocol.
Robin Hood placement does not prevent primary clustering, and fairness is not a promise that every operation performs less work than ordinary linear probing. Track mean, variance, maximum distance, lookup probes, insertion moves, and deletion shifts separately. The original Robin Hood literature also studies probing and search models beyond this simple linear scan; guarantees for those models should not be imported without checking their assumptions.
For long strings, hashing and key comparison have costs depending on length. For hostile input, the example's fixed deterministic mixer is not an adversarial performance defense. Its purpose is to make layout and counters exactly reproducible while the dictionary invariants remain directly testable.
Never clear a hole in the middle of a probe path without repair, and never swap a key without its DIB. Early termination requires the strict smaller-DIB test and a consistent deletion policy. Robin Hood balances displacements; it does not guarantee constant worst-case time, globally sorted DIBs, or correct metadata after a resize that merely copies slots.
Practice Problems and Real-World Use
Compare every public operation with an independent reference set. After each operation, verify multiplicities and size, recompute each resident's DIB from its home and slot, and check every earlier offset on its lookup path. Query absent keys as well as members so that an incorrect early-stop rule cannot hide behind successful lookups alone.
Include empty tables, duplicate insertion, absent deletion, repeated insert/delete cycles, zero and extreme integers, long shared-home clusters, a zero-DIB stopping boundary, wrapping at the last slot, growth, and a capacity-limit failure that leaves the prior state unchanged. For small tables, enumerate many assigned home patterns and insertion orders; this tests the displacement policy independently of one chosen hash function.
| Problem or system | Idea to implement or verify | Difficulty |
|---|---|---|
| Full sample trace | Match all $12$ placement probes, $4$ steals, and $3$ deletion shifts. | Beginner |
| Early-stop counterexample | Use the valid DIB sequence $[0,1,0]$ and explain why a query may stop at the drop. | Intermediate |
| Circular deletion laboratory | Create a cluster crossing the end of the array and shift survivors across slot $0$. | Intermediate |
| Fairness comparison | Compare ordinary and Robin Hood placement with identical homes; measure mean, variance, and maximum DIB separately. | Intermediate |
| In-memory cache index | Evaluate local array access, mutation cost, and latency across load factors. | Intermediate |
| Growth and metadata audit | Reinsert at doubled capacity, recompute all homes, and verify exact DIBs and memberships. | Advanced |
| Compact metadata design | Specify distance overflow behavior, empty markers, and any fingerprint filtering. | Advanced |
| Concurrent flat dictionary | Design synchronization so readers do not observe a pending key or temporary duplicate during relocation. | Research extension |
For further study, Pedro Celis's Robin Hood Hashing thesis develops the original idea and analysis. Stanford's hash-table assignment provides exercises on Robin Hood insertion, early lookup termination, and backward-shift deletion. Use those discussions alongside an explicit probing model and the concrete invariants above.