Below is a tiny hash table with a handful of buckets. Each key you add is run through a simple hash function and dropped into a bucket. When two keys land in the same bucket they chain together — that is a collision.
<p class="hint">Type a key and <b>Insert</b> it. The hash function picks a bucket; keys landing in the same bucket <b>chain</b> together — that is a collision. Watch the <b>load factor</b> climb, then <b>Resize</b> to double the buckets and re-hash.</p>
<div class="row">
<input id="key" type="text" placeholder="key e.g. cat" maxlength="12" />
<button id="ins" type="button">Insert</button>
<button id="rand" type="button">Insert 5 random</button>
<button id="resize" type="button" class="ghost">Resize ×2</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="stats" id="stats"></div>
<div id="table" class="table"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.row { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
input { font: 500 14px system-ui, sans-serif; padding: .4rem .6rem; border: 1px solid #adb1b8;
border-radius: 8px; width: 130px; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .92rem; font-weight: 600; margin: .3rem 0 .7rem; display: flex; gap: 1.2rem; flex-wrap: wrap; }
.stats .warn { color: #c92f3c; }
.stats .good { color: #0a7d33; }
.table { display: flex; flex-direction: column; gap: 4px; }
.bucket { display: flex; align-items: center; gap: 6px; }
.idx { width: 34px; height: 34px; flex: none; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; background: #e8eef3; color: #1d3557;
border: 1px solid #cdd9e3; border-radius: 8px; }
.chain { display: flex; gap: 4px; flex-wrap: wrap; min-height: 34px; align-items: center; }
.chip { padding: .28rem .55rem; font: 600 13px system-ui, sans-serif; border-radius: 7px;
background: #c9ccd1; border: 1px solid #adb1b8; }
.bucket.collide .chip { background: #e63946; border-color: #c92f3c; color: #fff; }
.bucket.collide .idx { background: #f7d6d9; border-color: #e0a8ad; }
.arrow { color: #888; font-size: .8rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A hash table with separate chaining. The hash function maps a string key to a bucket index; collisions (two keys, same bucket) are stored in a per-bucket list. Average lookup is O(1) while the load factor a = items / buckets stays small; let it grow and chains lengthen toward O(n). Resizing doubles buckets and re-hashes everything, restoring short chains. This is solved, classic data-structure theory.
let buckets = 4;
let table = Array.from({length: buckets}, () => []);
let count = 0;
// Simple deterministic string hash (polynomial rolling), then mod #buckets.
function hash(key, m) {
let h = 0;
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
return h % m;
}
function insert(key) {
key = key.trim();
if (!key) return;
const b = hash(key, buckets);
if (table[b].includes(key)) return; // no duplicates
table[b].push(key);
count++;
render();
}
function resize() {
const old = table;
buckets *= 2;
table = Array.from({length: buckets}, () => []);
for (const chain of old) for (const key of chain) table[hash(key, buckets)].push(key);
render();
}
function reset() {
buckets = 4;
table = Array.from({length: buckets}, () => []);
count = 0;
render();
}
function maxChain() { return table.reduce((m, c) => Math.max(m, c.length), 0); }
function render() {
const alpha = count / buckets;
const longest = maxChain();
const cls = alpha > 1 ? 'warn' : 'good';
document.getElementById('stats').innerHTML =
fmt('<span>Items: %count%</span><span>Buckets: %buckets%</span><span class="%cls%">Load factor α = %alpha%</span><span>Longest chain: %longest%</span>', { count: count, buckets: buckets, cls: cls, alpha: alpha.toFixed(2), longest: longest });
const t = document.getElementById('table');
t.innerHTML = '';
table.forEach((chain, i) => {
const row = document.createElement('div');
row.className = 'bucket' + (chain.length > 1 ? ' collide' : '');
const idx = document.createElement('div');
idx.className = 'idx'; idx.textContent = i;
const c = document.createElement('div');
c.className = 'chain';
chain.forEach((k, j) => {
if (j > 0) { const a = document.createElement('span'); a.className = 'arrow'; a.textContent = '→'; c.appendChild(a); }
const chip = document.createElement('span');
chip.className = 'chip'; chip.textContent = k; c.appendChild(chip);
});
row.appendChild(idx); row.appendChild(c);
t.appendChild(row);
});
}
const WORDS = ['cat','dog','sun','map','key','red','sky','owl','fox','ant','bee','ice','oak','pen','jam'];
document.getElementById('ins').addEventListener('click', () => {
insert(document.getElementById('key').value); document.getElementById('key').value = '';
});
document.getElementById('key').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('ins').click(); });
document.getElementById('rand').addEventListener('click', () => {
for (let i = 0; i < 5; i++) insert(WORDS[Math.floor(Math.random() * WORDS.length)] + Math.floor(Math.random() * 90));
});
document.getElementById('resize').addEventListener('click', resize);
document.getElementById('reset').addEventListener('click', reset);
render();
<p class="hint">Type a key and <b>Insert</b> it. The hash function picks a bucket; keys landing in the same bucket <b>chain</b> together — that is a collision. Watch the <b>load factor</b> climb, then <b>Resize</b> to double the buckets and re-hash.</p>
<div class="row">
<input id="key" type="text" placeholder="key e.g. cat" maxlength="12" />
<button id="ins" type="button">Insert</button>
<button id="rand" type="button">Insert 5 random</button>
<button id="resize" type="button" class="ghost">Resize ×2</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="stats" id="stats"></div>
<div id="table" class="table"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.row { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
input { font: 500 14px system-ui, sans-serif; padding: .4rem .6rem; border: 1px solid #adb1b8;
border-radius: 8px; width: 130px; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .92rem; font-weight: 600; margin: .3rem 0 .7rem; display: flex; gap: 1.2rem; flex-wrap: wrap; }
.stats .warn { color: #c92f3c; }
.stats .good { color: #0a7d33; }
.table { display: flex; flex-direction: column; gap: 4px; }
.bucket { display: flex; align-items: center; gap: 6px; }
.idx { width: 34px; height: 34px; flex: none; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; background: #e8eef3; color: #1d3557;
border: 1px solid #cdd9e3; border-radius: 8px; }
.chain { display: flex; gap: 4px; flex-wrap: wrap; min-height: 34px; align-items: center; }
.chip { padding: .28rem .55rem; font: 600 13px system-ui, sans-serif; border-radius: 7px;
background: #c9ccd1; border: 1px solid #adb1b8; }
.bucket.collide .chip { background: #e63946; border-color: #c92f3c; color: #fff; }
.bucket.collide .idx { background: #f7d6d9; border-color: #e0a8ad; }
.arrow { color: #888; font-size: .8rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A hash table with separate chaining. The hash function maps a string key to a bucket index; collisions (two keys, same bucket) are stored in a per-bucket list. Average lookup is O(1) while the load factor a = items / buckets stays small; let it grow and chains lengthen toward O(n). Resizing doubles buckets and re-hashes everything, restoring short chains. This is solved, classic data-structure theory.
let buckets = 4;
let table = Array.from({length: buckets}, () => []);
let count = 0;
// Simple deterministic string hash (polynomial rolling), then mod #buckets.
function hash(key, m) {
let h = 0;
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
return h % m;
}
function insert(key) {
key = key.trim();
if (!key) return;
const b = hash(key, buckets);
if (table[b].includes(key)) return; // no duplicates
table[b].push(key);
count++;
render();
}
function resize() {
const old = table;
buckets *= 2;
table = Array.from({length: buckets}, () => []);
for (const chain of old) for (const key of chain) table[hash(key, buckets)].push(key);
render();
}
function reset() {
buckets = 4;
table = Array.from({length: buckets}, () => []);
count = 0;
render();
}
function maxChain() { return table.reduce((m, c) => Math.max(m, c.length), 0); }
function render() {
const alpha = count / buckets;
const longest = maxChain();
const cls = alpha > 1 ? 'warn' : 'good';
document.getElementById('stats').innerHTML =
fmt('<span>Items: %count%</span><span>Buckets: %buckets%</span><span class="%cls%">Load factor α = %alpha%</span><span>Longest chain: %longest%</span>', { count: count, buckets: buckets, cls: cls, alpha: alpha.toFixed(2), longest: longest });
const t = document.getElementById('table');
t.innerHTML = '';
table.forEach((chain, i) => {
const row = document.createElement('div');
row.className = 'bucket' + (chain.length > 1 ? ' collide' : '');
const idx = document.createElement('div');
idx.className = 'idx'; idx.textContent = i;
const c = document.createElement('div');
c.className = 'chain';
chain.forEach((k, j) => {
if (j > 0) { const a = document.createElement('span'); a.className = 'arrow'; a.textContent = '→'; c.appendChild(a); }
const chip = document.createElement('span');
chip.className = 'chip'; chip.textContent = k; c.appendChild(chip);
});
row.appendChild(idx); row.appendChild(c);
t.appendChild(row);
});
}
const WORDS = ['cat','dog','sun','map','key','red','sky','owl','fox','ant','bee','ice','oak','pen','jam'];
document.getElementById('ins').addEventListener('click', () => {
insert(document.getElementById('key').value); document.getElementById('key').value = '';
});
document.getElementById('key').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('ins').click(); });
document.getElementById('rand').addEventListener('click', () => {
for (let i = 0; i < 5; i++) insert(WORDS[Math.floor(Math.random() * WORDS.length)] + Math.floor(Math.random() * 90));
});
document.getElementById('resize').addEventListener('click', resize);
document.getElementById('reset').addEventListener('click', reset);
render();
Watch the load factor (items ÷ buckets) climb as you insert. While it stays low, almost every bucket holds one item and lookup is effectively one step. Push it past 1 and chains grow, so a lookup must walk a list — drifting toward O(n). Hit Resize and the table doubles its buckets, re-hashes everything, and the chains melt away. That trade — extra space for fast access — is hashing in one move.
Comments
Loading comments...