Insert keys into the hash table below. Each slot shows the key it holds and its probe length — how many steps it took from its home slot to land here. Click Insert random key to add a new key and watch the Robin Hood rule displace shorter-probing keys to make room.
<div class="controls">
<button id="btn-insert" type="button">Insert random key</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="stat">Max probe: <b id="max-probe">0</b> | Keys: <b id="key-count">0</b> / <b id="table-size">0</b></span>
</div>
<div id="table-wrap">
<div id="hash-table"></div>
</div>
<div id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stat { font-size: .85rem; color: #555; margin-left: .3rem; }
#table-wrap { overflow-x: auto; }
#hash-table { display: flex; gap: 3px; min-width: max-content; margin-bottom: .6rem; }
.slot { width: 44px; min-height: 56px; border-radius: 7px; border: 1.5px solid #cdd9e3;
background: #f0f4f7; display: flex; flex-direction: column;
align-items: center; justify-content: center; font-size: 11px; transition: background .25s; }
.slot .idx { color: #8a9db5; font-size: 9px; margin-bottom: 2px; }
.slot .key { font: 700 12px ui-monospace, monospace; color: #1d3557; }
.slot .probe { font-size: 10px; color: #666; }
.slot.empty { background: #eef1f4; border-color: #e0e5ea; }
.slot.filled { background: #dce8f3; border-color: #b0c8e0; }
.slot.just-inserted { background: #c6e0b8; border-color: #7db86a; }
.slot.displaced { background: #fde9c5; border-color: #e8a43a; }
.slot.probe-0 { border-color: #7db86a; }
.slot.probe-high { border-color: #e8703a; }
#log { font-size: .8rem; color: #444; min-height: 2.4em; line-height: 1.5; }
.log-ins { color: #0a7d33; }
.log-disp { color: #b85e00; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Robin Hood Hashing — open addressing with linear probing + displacement swap.
// Rule: when inserting key K at slot s, if the current occupant C has a SHORTER
// probe length than K (C is "richer"), swap K and C; C continues probing.
// This keeps the variance of probe lengths minimal (Celis 1986).
const SIZE = 13; // prime table size
let table = new Array(SIZE).fill(null); // null = empty; {key, home, probe}
let keyCounter = 0;
// Simple hash: djb2-style on the string, mod SIZE
function hash(key) {
let h = 5381;
for (let i = 0; i < key.length; i++) h = ((h << 5) + h + key.charCodeAt(i)) >>> 0;
return h % SIZE;
}
function probeLen(slot, home) {
return (slot - home + SIZE) % SIZE;
}
// Insert key using Robin Hood hashing. Returns {inserted, steps, log}.
function rhInsert(key) {
const home = hash(key);
let slot = home;
let currentKey = key;
let currentHome = home;
let steps = 0;
const events = [];
for (let i = 0; i < SIZE; i++) {
const pos = (home + i) % SIZE;
steps++;
if (table[pos] === null) {
// Empty slot — place it
table[pos] = { key: currentKey, home: currentHome, probe: probeLen(pos, currentHome) };
events.push({ type: 'insert', pos, key: currentKey, probe: probeLen(pos, currentHome) });
return { inserted: true, steps, events };
}
const resident = table[pos];
const residentProbe = probeLen(pos, resident.home);
const currentProbe = probeLen(pos, currentHome);
if (currentProbe > residentProbe) {
// Robin Hood: steal the slot from the richer resident
events.push({ type: 'displace', pos, evicted: resident.key, placed: currentKey, probe: currentProbe });
table[pos] = { key: currentKey, home: currentHome, probe: currentProbe };
// Continue inserting the displaced resident
currentKey = resident.key;
currentHome = resident.home;
}
// else: current probe <= resident probe, keep probing
}
return { inserted: false, steps, events }; // table full
}
function maxProbe() {
return table.reduce((m, s) => s ? Math.max(m, s.probe) : m, 0);
}
function keyCount() {
return table.filter(Boolean).length;
}
function render(highlightPos, displaced) {
const el = document.getElementById('hash-table');
el.innerHTML = '';
table.forEach((slot, i) => {
const div = document.createElement('div');
let cls = 'slot';
if (!slot) {
cls += ' empty';
} else {
cls += ' filled';
if (slot.probe === 0) cls += ' probe-0';
if (slot.probe >= 3) cls += ' probe-high';
}
if (i === highlightPos && slot && !displaced) cls += ' just-inserted';
if (displaced && displaced.includes(i)) cls += ' displaced';
div.className = cls;
const idxDiv = document.createElement('div');
idxDiv.className = 'idx'; idxDiv.textContent = i;
div.appendChild(idxDiv);
if (slot) {
const keyDiv = document.createElement('div');
keyDiv.className = 'key'; keyDiv.textContent = slot.key;
const probeDiv = document.createElement('div');
probeDiv.className = 'probe'; probeDiv.textContent = fmt('p=%probe%', { probe: slot.probe });
div.appendChild(keyDiv); div.appendChild(probeDiv);
}
el.appendChild(div);
});
document.getElementById('max-probe').textContent = maxProbe();
document.getElementById('key-count').textContent = keyCount();
document.getElementById('table-size').textContent = SIZE;
}
function setLog(html) {
document.getElementById('log').innerHTML = html;
}
const WORDS = ['alpha','beta','gamma','delta','epsilon','zeta','eta','theta',
'iota','kappa','lambda','mu','nu','xi','omicron','pi','rho',
'sigma','tau','upsilon'];
document.getElementById('btn-insert').addEventListener('click', () => {
if (keyCount() >= SIZE - 1) { setLog('Table is nearly full — reset to continue.'); return; }
const key = WORDS[keyCounter % WORDS.length];
keyCounter++;
const { inserted, events } = rhInsert(key);
if (!inserted) { setLog('Table full.'); return; }
const lastEvent = events[events.length - 1];
const displacedPositions = events.filter(e => e.type === 'displace').map(e => e.pos);
render(lastEvent.pos, displacedPositions.length ? displacedPositions : null);
let logParts = [fmt('<span class="log-ins">Inserted <b>%key%</b> (home=%hash%, probe=%probe%)</span>', { key: key, hash: hash(key), probe: lastEvent.probe })];
const displacedEvents = events.filter(e => e.type === 'displace');
if (displacedEvents.length) {
logParts.push(...displacedEvents.map(e =>
fmt('<span class="log-disp"> ↳ displaced <b>%evicted%</b> from slot %pos%</span>', { evicted: e.evicted, pos: e.pos })));
}
setLog(logParts.join('<br>'));
// Verify Robin Hood invariant: for every filled slot s,
// no filled slot t just before s (in probe order from t's home) has a longer probe.
// Simplified check: for each slot s with probe p>0, the slot at (s-1+SIZE)%SIZE
let invariantOk = true;
for (let i = 0; i < SIZE; i++) {
if (!table[i]) continue;
const p = table[i].probe;
if (p === 0) continue;
const prev = (i - 1 + SIZE) % SIZE;
if (!table[prev]) { invariantOk = false; break; } // gap before a displaced key is impossible
if (table[prev].probe < p - 1) { invariantOk = false; break; }
}
console.assert(invariantOk, 'Robin Hood invariant violated!');
});
document.getElementById('btn-reset').addEventListener('click', () => {
table = new Array(SIZE).fill(null);
keyCounter = 0;
render(null, null);
setLog('');
});
render(null, null);
<div class="controls">
<button id="btn-insert" type="button">Insert random key</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="stat">Max probe: <b id="max-probe">0</b> | Keys: <b id="key-count">0</b> / <b id="table-size">0</b></span>
</div>
<div id="table-wrap">
<div id="hash-table"></div>
</div>
<div id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stat { font-size: .85rem; color: #555; margin-left: .3rem; }
#table-wrap { overflow-x: auto; }
#hash-table { display: flex; gap: 3px; min-width: max-content; margin-bottom: .6rem; }
.slot { width: 44px; min-height: 56px; border-radius: 7px; border: 1.5px solid #cdd9e3;
background: #f0f4f7; display: flex; flex-direction: column;
align-items: center; justify-content: center; font-size: 11px; transition: background .25s; }
.slot .idx { color: #8a9db5; font-size: 9px; margin-bottom: 2px; }
.slot .key { font: 700 12px ui-monospace, monospace; color: #1d3557; }
.slot .probe { font-size: 10px; color: #666; }
.slot.empty { background: #eef1f4; border-color: #e0e5ea; }
.slot.filled { background: #dce8f3; border-color: #b0c8e0; }
.slot.just-inserted { background: #c6e0b8; border-color: #7db86a; }
.slot.displaced { background: #fde9c5; border-color: #e8a43a; }
.slot.probe-0 { border-color: #7db86a; }
.slot.probe-high { border-color: #e8703a; }
#log { font-size: .8rem; color: #444; min-height: 2.4em; line-height: 1.5; }
.log-ins { color: #0a7d33; }
.log-disp { color: #b85e00; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Robin Hood Hashing — open addressing with linear probing + displacement swap.
// Rule: when inserting key K at slot s, if the current occupant C has a SHORTER
// probe length than K (C is "richer"), swap K and C; C continues probing.
// This keeps the variance of probe lengths minimal (Celis 1986).
const SIZE = 13; // prime table size
let table = new Array(SIZE).fill(null); // null = empty; {key, home, probe}
let keyCounter = 0;
// Simple hash: djb2-style on the string, mod SIZE
function hash(key) {
let h = 5381;
for (let i = 0; i < key.length; i++) h = ((h << 5) + h + key.charCodeAt(i)) >>> 0;
return h % SIZE;
}
function probeLen(slot, home) {
return (slot - home + SIZE) % SIZE;
}
// Insert key using Robin Hood hashing. Returns {inserted, steps, log}.
function rhInsert(key) {
const home = hash(key);
let slot = home;
let currentKey = key;
let currentHome = home;
let steps = 0;
const events = [];
for (let i = 0; i < SIZE; i++) {
const pos = (home + i) % SIZE;
steps++;
if (table[pos] === null) {
// Empty slot — place it
table[pos] = { key: currentKey, home: currentHome, probe: probeLen(pos, currentHome) };
events.push({ type: 'insert', pos, key: currentKey, probe: probeLen(pos, currentHome) });
return { inserted: true, steps, events };
}
const resident = table[pos];
const residentProbe = probeLen(pos, resident.home);
const currentProbe = probeLen(pos, currentHome);
if (currentProbe > residentProbe) {
// Robin Hood: steal the slot from the richer resident
events.push({ type: 'displace', pos, evicted: resident.key, placed: currentKey, probe: currentProbe });
table[pos] = { key: currentKey, home: currentHome, probe: currentProbe };
// Continue inserting the displaced resident
currentKey = resident.key;
currentHome = resident.home;
}
// else: current probe <= resident probe, keep probing
}
return { inserted: false, steps, events }; // table full
}
function maxProbe() {
return table.reduce((m, s) => s ? Math.max(m, s.probe) : m, 0);
}
function keyCount() {
return table.filter(Boolean).length;
}
function render(highlightPos, displaced) {
const el = document.getElementById('hash-table');
el.innerHTML = '';
table.forEach((slot, i) => {
const div = document.createElement('div');
let cls = 'slot';
if (!slot) {
cls += ' empty';
} else {
cls += ' filled';
if (slot.probe === 0) cls += ' probe-0';
if (slot.probe >= 3) cls += ' probe-high';
}
if (i === highlightPos && slot && !displaced) cls += ' just-inserted';
if (displaced && displaced.includes(i)) cls += ' displaced';
div.className = cls;
const idxDiv = document.createElement('div');
idxDiv.className = 'idx'; idxDiv.textContent = i;
div.appendChild(idxDiv);
if (slot) {
const keyDiv = document.createElement('div');
keyDiv.className = 'key'; keyDiv.textContent = slot.key;
const probeDiv = document.createElement('div');
probeDiv.className = 'probe'; probeDiv.textContent = fmt('p=%probe%', { probe: slot.probe });
div.appendChild(keyDiv); div.appendChild(probeDiv);
}
el.appendChild(div);
});
document.getElementById('max-probe').textContent = maxProbe();
document.getElementById('key-count').textContent = keyCount();
document.getElementById('table-size').textContent = SIZE;
}
function setLog(html) {
document.getElementById('log').innerHTML = html;
}
const WORDS = ['alpha','beta','gamma','delta','epsilon','zeta','eta','theta',
'iota','kappa','lambda','mu','nu','xi','omicron','pi','rho',
'sigma','tau','upsilon'];
document.getElementById('btn-insert').addEventListener('click', () => {
if (keyCount() >= SIZE - 1) { setLog('Table is nearly full — reset to continue.'); return; }
const key = WORDS[keyCounter % WORDS.length];
keyCounter++;
const { inserted, events } = rhInsert(key);
if (!inserted) { setLog('Table full.'); return; }
const lastEvent = events[events.length - 1];
const displacedPositions = events.filter(e => e.type === 'displace').map(e => e.pos);
render(lastEvent.pos, displacedPositions.length ? displacedPositions : null);
let logParts = [fmt('<span class="log-ins">Inserted <b>%key%</b> (home=%hash%, probe=%probe%)</span>', { key: key, hash: hash(key), probe: lastEvent.probe })];
const displacedEvents = events.filter(e => e.type === 'displace');
if (displacedEvents.length) {
logParts.push(...displacedEvents.map(e =>
fmt('<span class="log-disp"> ↳ displaced <b>%evicted%</b> from slot %pos%</span>', { evicted: e.evicted, pos: e.pos })));
}
setLog(logParts.join('<br>'));
// Verify Robin Hood invariant: for every filled slot s,
// no filled slot t just before s (in probe order from t's home) has a longer probe.
// Simplified check: for each slot s with probe p>0, the slot at (s-1+SIZE)%SIZE
let invariantOk = true;
for (let i = 0; i < SIZE; i++) {
if (!table[i]) continue;
const p = table[i].probe;
if (p === 0) continue;
const prev = (i - 1 + SIZE) % SIZE;
if (!table[prev]) { invariantOk = false; break; } // gap before a displaced key is impossible
if (table[prev].probe < p - 1) { invariantOk = false; break; }
}
console.assert(invariantOk, 'Robin Hood invariant violated!');
});
document.getElementById('btn-reset').addEventListener('click', () => {
table = new Array(SIZE).fill(null);
keyCounter = 0;
render(null, null);
setLog('');
});
render(null, null);
Notice how the probe lengths stay tightly clustered. In a plain linear-probing table the unluckiest key would sit far from its home while others stay close — Robin Hood constantly re-balances, stealing from the short-probers and giving to the long ones. The max probe length shown at the top is always small even as the table fills up.
Comments
Loading comments...