<p class="hint">Enter up to 10 comma-separated keys, then click <b>Build MPH</b>. Each key gets mapped to a unique slot in 0..n-1 — no collisions, no gaps.</p>
<div class="input-row">
<input id="keys-input" type="text" value="apple,banana,cherry,date,elderberry,fig" placeholder="key1,key2,key3,..." />
<button id="build-btn" type="button">Build MPH</button>
</div>
<div id="error-msg" class="error-msg"></div>
<div id="result-area" class="result-area" style="display:none">
<div class="section-label">Hash table (slot → key)</div>
<div id="slots-grid" class="slots-grid"></div>
<div class="section-label" style="margin-top:1rem">Verify a lookup</div>
<div class="lookup-row">
<input id="lookup-input" type="text" placeholder="Type a key to look up…" />
<button id="lookup-btn" type="button">Look up</button>
</div>
<div id="lookup-result" class="lookup-result"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.input-row { display: flex; gap: .5rem; flex-wrap: wrap; }
#keys-input { flex: 1; min-width: 0; padding: .42rem .7rem; border: 1px solid #adb1b8;
border-radius: 8px; font: inherit; font-size: .9rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button:hover { background: #16294a; }
.error-msg { color: #c92f3c; font-size: .88rem; min-height: 1.3em; margin-top: .3rem; }
.result-area { margin-top: .9rem; }
.section-label { font-size: .8rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #6b7280; margin-bottom: .4rem; }
.slots-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.slot { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 80px; min-height: 60px; border-radius: 10px; border: 1.5px solid #cdd9e3;
background: #eaf1f7; padding: 4px 6px; text-align: center; transition: background .15s; }
.slot.active { background: #d0f0db; border-color: #4caf7a; }
.slot.highlight { background: #ffe87a; border-color: #c9a800; }
.slot-index { font-size: .72rem; font-weight: 700; color: #6b7280; margin-bottom: 2px; }
.slot-key { font-size: .78rem; font-weight: 600; color: #1d3557; word-break: break-all; line-height: 1.2; }
.lookup-row { display: flex; gap: .5rem; flex-wrap: wrap; }
#lookup-input { flex: 1; min-width: 0; padding: .42rem .7rem; border: 1px solid #adb1b8;
border-radius: 8px; font: inherit; font-size: .9rem; }
.lookup-result { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; }
.lookup-result.ok { color: #0a7d33; }
.lookup-result.miss { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minimal Perfect Hash — CHD-style two-level construction
// Level 1: a pilot hash h1(key, seed) assigns each key to a "bucket".
// Level 2: for each bucket (sorted by size desc), find an offset d such that
// h2(key, d) lands in a free slot for every key in the bucket.
// Lookup: slot = h2(key, d[h1(key, seed)])
//
// This is a simplified CHD (Compress, Hash, Displace).
// All hashing uses a simple polynomial rolling hash modulo n (or bucket count).
function polyHash(str, seed, mod) {
// Deterministic "random" multiplier from seed
let h = seed ^ 0xdeadbeef;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 0x9e3779b9 + i);
h ^= h >>> 16;
}
h = Math.imul(h ^ (seed * 0x45d9f3b), 0x119de1f3);
h ^= h >>> 16;
return ((h >>> 0) % mod + mod) % mod;
}
function buildMPH(keys) {
const n = keys.length;
if (n === 0) return null;
const NUM_BUCKETS = Math.max(1, Math.ceil(n / 2)); // ~n/2 buckets → avg ~2 keys/bucket
// Try different seeds for the pilot hash until we find one with no empty buckets
// causing issues. In practice the first seed almost always works for small n.
let seed = 1, buckets, maxTries = 200;
outer:
for (let attempt = 0; attempt < maxTries; attempt++, seed++) {
buckets = Array.from({ length: NUM_BUCKETS }, () => []);
for (const k of keys) {
buckets[polyHash(k, seed, NUM_BUCKETS)].push(k);
}
// Sort buckets by size descending (larger buckets are harder; place them first)
const order = Array.from({ length: NUM_BUCKETS }, (_, i) => i)
.sort((a, b) => buckets[b].length - buckets[a].length);
const slots = new Array(n).fill(null); // final table
const d = new Array(NUM_BUCKETS).fill(0); // per-bucket offset
let ok = true;
for (const bi of order) {
const bkeys = buckets[bi];
if (bkeys.length === 0) { d[bi] = 0; continue; }
let found = false;
for (let offset = 0; offset < n * 4 + 10; offset++) {
// Try placing all keys in bucket bi with this offset
const taken = [];
let conflict = false;
for (const k of bkeys) {
const slot = polyHash(k, offset, n);
if (slots[slot] !== null || taken.includes(slot)) { conflict = true; break; }
taken.push(slot);
}
if (!conflict) {
// Commit
for (let i = 0; i < bkeys.length; i++) slots[taken[i]] = bkeys[i];
d[bi] = offset;
found = true;
break;
}
}
if (!found) { ok = false; break; }
}
if (ok) {
return { seed, d, slots, n, NUM_BUCKETS };
}
}
return null; // failed (shouldn't happen for n ≤ 10)
}
function mphLookup(key, mph) {
const bi = polyHash(key, mph.seed, mph.NUM_BUCKETS);
const slot = polyHash(key, mph.d[bi], mph.n);
return { slot, stored: mph.slots[slot] };
}
// ── UI ──────────────────────────────────────────────────────────────────────
let currentMPH = null;
let currentHighlight = -1;
function renderSlots(highlight = -1) {
if (!currentMPH) return;
currentHighlight = highlight;
const grid = document.getElementById('slots-grid');
grid.innerHTML = '';
currentMPH.slots.forEach((key, i) => {
const div = document.createElement('div');
div.className = 'slot' + (key ? ' active' : '') + (i === highlight ? ' highlight' : '');
const idx = document.createElement('div'); idx.className = 'slot-index'; idx.textContent = fmt('slot %i%', { i: i });
const kd = document.createElement('div'); kd.className = 'slot-key'; kd.textContent = key || '—';
div.appendChild(idx); div.appendChild(kd);
grid.appendChild(div);
});
}
document.getElementById('build-btn').addEventListener('click', () => {
const raw = document.getElementById('keys-input').value;
const keys = raw.split(',').map(s => s.trim()).filter(Boolean);
const errEl = document.getElementById('error-msg');
const resultArea = document.getElementById('result-area');
const lookupRes = document.getElementById('lookup-result');
errEl.textContent = '';
lookupRes.textContent = '';
lookupRes.className = 'lookup-result';
if (keys.length === 0) { errEl.textContent = 'Enter at least one key.'; return; }
if (keys.length > 10) { errEl.textContent = 'Max 10 keys in this demo.'; return; }
const uniq = new Set(keys);
if (uniq.size !== keys.length) { errEl.textContent = 'Keys must be distinct.'; return; }
const mph = buildMPH(keys);
if (!mph) { errEl.textContent = 'Construction failed — try different keys.'; return; }
currentMPH = mph;
resultArea.style.display = '';
renderSlots(-1);
// Verify correctness internally
let allOk = true;
for (const k of keys) {
const { slot, stored } = mphLookup(k, mph);
if (stored !== k) { allOk = false; break; }
}
console.log(fmt('MPH built. n=%length% seed=%seed% allCorrect=%allOk%', { length: keys.length, seed: mph.seed, allOk: allOk }));
console.log('Slots:', mph.slots.join(', '));
});
document.getElementById('lookup-btn').addEventListener('click', () => {
if (!currentMPH) return;
const key = document.getElementById('lookup-input').value.trim();
const res = document.getElementById('lookup-result');
if (!key) { res.textContent = 'Enter a key to look up.'; res.className = 'lookup-result'; return; }
const { slot, stored } = mphLookup(key, currentMPH);
renderSlots(slot);
if (stored === key) {
res.textContent = fmt('✓ "%key%" → slot %slot% (found!)', { key: key, slot: slot });
res.className = 'lookup-result ok';
} else {
res.textContent = fmt('✗ "%key%" → slot %slot% but stored"%v%" — key not in set', { key: key, slot: slot, v: stored || '—' });
res.className = 'lookup-result miss';
}
});
// Trigger initial build
document.getElementById('build-btn').click();
<p class="hint">Enter up to 10 comma-separated keys, then click <b>Build MPH</b>. Each key gets mapped to a unique slot in 0..n-1 — no collisions, no gaps.</p>
<div class="input-row">
<input id="keys-input" type="text" value="apple,banana,cherry,date,elderberry,fig" placeholder="key1,key2,key3,..." />
<button id="build-btn" type="button">Build MPH</button>
</div>
<div id="error-msg" class="error-msg"></div>
<div id="result-area" class="result-area" style="display:none">
<div class="section-label">Hash table (slot → key)</div>
<div id="slots-grid" class="slots-grid"></div>
<div class="section-label" style="margin-top:1rem">Verify a lookup</div>
<div class="lookup-row">
<input id="lookup-input" type="text" placeholder="Type a key to look up…" />
<button id="lookup-btn" type="button">Look up</button>
</div>
<div id="lookup-result" class="lookup-result"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.input-row { display: flex; gap: .5rem; flex-wrap: wrap; }
#keys-input { flex: 1; min-width: 0; padding: .42rem .7rem; border: 1px solid #adb1b8;
border-radius: 8px; font: inherit; font-size: .9rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button:hover { background: #16294a; }
.error-msg { color: #c92f3c; font-size: .88rem; min-height: 1.3em; margin-top: .3rem; }
.result-area { margin-top: .9rem; }
.section-label { font-size: .8rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #6b7280; margin-bottom: .4rem; }
.slots-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.slot { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 80px; min-height: 60px; border-radius: 10px; border: 1.5px solid #cdd9e3;
background: #eaf1f7; padding: 4px 6px; text-align: center; transition: background .15s; }
.slot.active { background: #d0f0db; border-color: #4caf7a; }
.slot.highlight { background: #ffe87a; border-color: #c9a800; }
.slot-index { font-size: .72rem; font-weight: 700; color: #6b7280; margin-bottom: 2px; }
.slot-key { font-size: .78rem; font-weight: 600; color: #1d3557; word-break: break-all; line-height: 1.2; }
.lookup-row { display: flex; gap: .5rem; flex-wrap: wrap; }
#lookup-input { flex: 1; min-width: 0; padding: .42rem .7rem; border: 1px solid #adb1b8;
border-radius: 8px; font: inherit; font-size: .9rem; }
.lookup-result { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.4em; }
.lookup-result.ok { color: #0a7d33; }
.lookup-result.miss { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Minimal Perfect Hash — CHD-style two-level construction
// Level 1: a pilot hash h1(key, seed) assigns each key to a "bucket".
// Level 2: for each bucket (sorted by size desc), find an offset d such that
// h2(key, d) lands in a free slot for every key in the bucket.
// Lookup: slot = h2(key, d[h1(key, seed)])
//
// This is a simplified CHD (Compress, Hash, Displace).
// All hashing uses a simple polynomial rolling hash modulo n (or bucket count).
function polyHash(str, seed, mod) {
// Deterministic "random" multiplier from seed
let h = seed ^ 0xdeadbeef;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 0x9e3779b9 + i);
h ^= h >>> 16;
}
h = Math.imul(h ^ (seed * 0x45d9f3b), 0x119de1f3);
h ^= h >>> 16;
return ((h >>> 0) % mod + mod) % mod;
}
function buildMPH(keys) {
const n = keys.length;
if (n === 0) return null;
const NUM_BUCKETS = Math.max(1, Math.ceil(n / 2)); // ~n/2 buckets → avg ~2 keys/bucket
// Try different seeds for the pilot hash until we find one with no empty buckets
// causing issues. In practice the first seed almost always works for small n.
let seed = 1, buckets, maxTries = 200;
outer:
for (let attempt = 0; attempt < maxTries; attempt++, seed++) {
buckets = Array.from({ length: NUM_BUCKETS }, () => []);
for (const k of keys) {
buckets[polyHash(k, seed, NUM_BUCKETS)].push(k);
}
// Sort buckets by size descending (larger buckets are harder; place them first)
const order = Array.from({ length: NUM_BUCKETS }, (_, i) => i)
.sort((a, b) => buckets[b].length - buckets[a].length);
const slots = new Array(n).fill(null); // final table
const d = new Array(NUM_BUCKETS).fill(0); // per-bucket offset
let ok = true;
for (const bi of order) {
const bkeys = buckets[bi];
if (bkeys.length === 0) { d[bi] = 0; continue; }
let found = false;
for (let offset = 0; offset < n * 4 + 10; offset++) {
// Try placing all keys in bucket bi with this offset
const taken = [];
let conflict = false;
for (const k of bkeys) {
const slot = polyHash(k, offset, n);
if (slots[slot] !== null || taken.includes(slot)) { conflict = true; break; }
taken.push(slot);
}
if (!conflict) {
// Commit
for (let i = 0; i < bkeys.length; i++) slots[taken[i]] = bkeys[i];
d[bi] = offset;
found = true;
break;
}
}
if (!found) { ok = false; break; }
}
if (ok) {
return { seed, d, slots, n, NUM_BUCKETS };
}
}
return null; // failed (shouldn't happen for n ≤ 10)
}
function mphLookup(key, mph) {
const bi = polyHash(key, mph.seed, mph.NUM_BUCKETS);
const slot = polyHash(key, mph.d[bi], mph.n);
return { slot, stored: mph.slots[slot] };
}
// ── UI ──────────────────────────────────────────────────────────────────────
let currentMPH = null;
let currentHighlight = -1;
function renderSlots(highlight = -1) {
if (!currentMPH) return;
currentHighlight = highlight;
const grid = document.getElementById('slots-grid');
grid.innerHTML = '';
currentMPH.slots.forEach((key, i) => {
const div = document.createElement('div');
div.className = 'slot' + (key ? ' active' : '') + (i === highlight ? ' highlight' : '');
const idx = document.createElement('div'); idx.className = 'slot-index'; idx.textContent = fmt('slot %i%', { i: i });
const kd = document.createElement('div'); kd.className = 'slot-key'; kd.textContent = key || '—';
div.appendChild(idx); div.appendChild(kd);
grid.appendChild(div);
});
}
document.getElementById('build-btn').addEventListener('click', () => {
const raw = document.getElementById('keys-input').value;
const keys = raw.split(',').map(s => s.trim()).filter(Boolean);
const errEl = document.getElementById('error-msg');
const resultArea = document.getElementById('result-area');
const lookupRes = document.getElementById('lookup-result');
errEl.textContent = '';
lookupRes.textContent = '';
lookupRes.className = 'lookup-result';
if (keys.length === 0) { errEl.textContent = 'Enter at least one key.'; return; }
if (keys.length > 10) { errEl.textContent = 'Max 10 keys in this demo.'; return; }
const uniq = new Set(keys);
if (uniq.size !== keys.length) { errEl.textContent = 'Keys must be distinct.'; return; }
const mph = buildMPH(keys);
if (!mph) { errEl.textContent = 'Construction failed — try different keys.'; return; }
currentMPH = mph;
resultArea.style.display = '';
renderSlots(-1);
// Verify correctness internally
let allOk = true;
for (const k of keys) {
const { slot, stored } = mphLookup(k, mph);
if (stored !== k) { allOk = false; break; }
}
console.log(fmt('MPH built. n=%length% seed=%seed% allCorrect=%allOk%', { length: keys.length, seed: mph.seed, allOk: allOk }));
console.log('Slots:', mph.slots.join(', '));
});
document.getElementById('lookup-btn').addEventListener('click', () => {
if (!currentMPH) return;
const key = document.getElementById('lookup-input').value.trim();
const res = document.getElementById('lookup-result');
if (!key) { res.textContent = 'Enter a key to look up.'; res.className = 'lookup-result'; return; }
const { slot, stored } = mphLookup(key, currentMPH);
renderSlots(slot);
if (stored === key) {
res.textContent = fmt('✓ "%key%" → slot %slot% (found!)', { key: key, slot: slot });
res.className = 'lookup-result ok';
} else {
res.textContent = fmt('✗ "%key%" → slot %slot% but stored"%v%" — key not in set', { key: key, slot: slot, v: stored || '—' });
res.className = 'lookup-result miss';
}
});
// Trigger initial build
document.getElementById('build-btn').click();
Comments
Loading comments...