<p class="hint">A Count-Min sketch with <b>4 rows × 8 columns</b> — just 32 counters. Each item is hashed to one cell per row and that cell is bumped by one. To estimate a count, read the same cells and take the <b>minimum</b>. The estimate is never below the true count, but collisions can push it above.</p>
<div class="ctrl">
<input id="item" type="text" value="apple" maxlength="14" />
<button id="add" type="button">Add item</button>
<button id="add5" type="button">Add ×5</button>
<button id="stream" type="button" class="ghost">Random stream ×40</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="grid" class="grid"></div>
<div class="query">
<button id="q" type="button">Query "<span id="qname">apple</span>"</button>
</div>
<div class="status" id="status">Add some items, then query.</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; }
.ctrl { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
input { font: 600 14px ui-monospace, monospace; padding: .4rem .6rem; border: 1px solid #adb1b8;
border-radius: 8px; width: 120px; }
.grid { display: grid; grid-template-columns: 64px repeat(8, 1fr); gap: 4px; margin: .4rem 0; max-width: 560px; }
.lbl { display: flex; align-items: center; font: 600 12px system-ui, sans-serif; color: #1d3557; }
.cell { aspect-ratio: 1; min-height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 6px; background: #eef2f6;
border: 1px solid #cdd9e3; color: #1d3557; transition: background .25s, color .25s; }
.cell.hot { background: #1d3557; color: #fff; border-color: #14233b; }
.cell.pick { outline: 3px solid #e9b949; outline-offset: -3px; }
.query { margin: .6rem 0; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status b { color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Count-Min sketch (Cormode & Muthukrishnan, 2005). D rows, W columns.
// add(x): for each row r, increment grid[r][h_r(x) mod W].
// query(x): estimate = min over rows of grid[r][h_r(x) mod W].
// The estimate is never below the true count: every add only increments,
// and collisions can only push a cell higher, so the per-row reads are all >= true count, hence their minimum is too.
const D = 4, W = 8;
let grid, truth;
const SEEDS = [0x9e37, 0x85eb, 0xc2b2, 0x27d4];
// Simple deterministic string hash, mixed per-row by a seed.
function hash(str, seed){
let h = seed >>> 0;
for (let i = 0; i < str.length; i++){
h = (h ^ str.charCodeAt(i)) >>> 0;
h = Math.imul(h, 0x01000193) >>> 0;
}
h ^= h >>> 15;
return (h >>> 0) % W;
}
function cols(str){ return SEEDS.map(s => hash(str, s)); }
function reset(){
grid = Array.from({length: D}, () => new Array(W).fill(0));
truth = {};
render([]);
setStatus('Add some items, then query.', '');
}
function add(str, n){
const c = cols(str);
for (let t = 0; t < n; t++){
for (let r = 0; r < D; r++) grid[r][c[r]]++;
truth[str] = (truth[str] || 0) + 1;
}
render(c);
}
function estimate(str){
const c = cols(str);
let m = Infinity;
for (let r = 0; r < D; r++) m = Math.min(m, grid[r][c[r]]);
return { est: m, cols: c };
}
function render(active){
const g = document.getElementById('grid');
g.innerHTML = '';
for (let r = 0; r < D; r++){
const lbl = document.createElement('div');
lbl.className = 'lbl'; lbl.textContent = fmt('row %r%%v%', { r: r, v: 1 });
g.appendChild(lbl);
for (let col = 0; col < W; col++){
const d = document.createElement('div');
const v = grid[r][col];
d.className = 'cell' + (v > 0 ? ' hot' : '') + (active[r] === col ? ' pick' : '');
d.textContent = v;
g.appendChild(d);
}
}
}
function setStatus(t, cls){
const s = document.getElementById('status'); s.innerHTML = t; s.className = 'status ' + cls;
}
const POOL = ['apple','banana','cherry','date','fig','grape','kiwi','lemon','mango','pear'];
document.getElementById('add').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (v){ add(v, 1); document.getElementById('qname').textContent = v; }
});
document.getElementById('add5').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (v){ add(v, 5); document.getElementById('qname').textContent = v; }
});
document.getElementById('stream').addEventListener('click', () => {
for (let i = 0; i < 40; i++) add(POOL[Math.floor(Math.random() * POOL.length)], 1);
});
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('item').addEventListener('input', e => {
document.getElementById('qname').textContent = (e.target.value || '').trim() || '?';
});
document.getElementById('q').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (!v){ setStatus('Type an item to query.', ''); return; }
const { est, cols: c } = estimate(v);
render(c);
const real = truth[v] || 0;
const over = est - real;
const note = over === 0 ? 'exactly right — no collisions hit it.'
: fmt('overestimate by %over% (collisions), but never below the truth.', { over: over });
setStatus(fmt('Estimate for "<b>%v%</b>": <b>%est%</b> True count: <b>%real%</b> — %note%', { v: v, est: est, real: real, note: note }), 'ok');
console.log('query', v, '-> estimate', est, '| true', real, '| cells', c.join(','));
});
reset();
<p class="hint">A Count-Min sketch with <b>4 rows × 8 columns</b> — just 32 counters. Each item is hashed to one cell per row and that cell is bumped by one. To estimate a count, read the same cells and take the <b>minimum</b>. The estimate is never below the true count, but collisions can push it above.</p>
<div class="ctrl">
<input id="item" type="text" value="apple" maxlength="14" />
<button id="add" type="button">Add item</button>
<button id="add5" type="button">Add ×5</button>
<button id="stream" type="button" class="ghost">Random stream ×40</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="grid" class="grid"></div>
<div class="query">
<button id="q" type="button">Query "<span id="qname">apple</span>"</button>
</div>
<div class="status" id="status">Add some items, then query.</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; }
.ctrl { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; margin-bottom: .8rem; }
input { font: 600 14px ui-monospace, monospace; padding: .4rem .6rem; border: 1px solid #adb1b8;
border-radius: 8px; width: 120px; }
.grid { display: grid; grid-template-columns: 64px repeat(8, 1fr); gap: 4px; margin: .4rem 0; max-width: 560px; }
.lbl { display: flex; align-items: center; font: 600 12px system-ui, sans-serif; color: #1d3557; }
.cell { aspect-ratio: 1; min-height: 34px; display: flex; align-items: center; justify-content: center;
font: 700 14px ui-monospace, monospace; border-radius: 6px; background: #eef2f6;
border: 1px solid #cdd9e3; color: #1d3557; transition: background .25s, color .25s; }
.cell.hot { background: #1d3557; color: #fff; border-color: #14233b; }
.cell.pick { outline: 3px solid #e9b949; outline-offset: -3px; }
.query { margin: .6rem 0; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status b { color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Count-Min sketch (Cormode & Muthukrishnan, 2005). D rows, W columns.
// add(x): for each row r, increment grid[r][h_r(x) mod W].
// query(x): estimate = min over rows of grid[r][h_r(x) mod W].
// The estimate is never below the true count: every add only increments,
// and collisions can only push a cell higher, so the per-row reads are all >= true count, hence their minimum is too.
const D = 4, W = 8;
let grid, truth;
const SEEDS = [0x9e37, 0x85eb, 0xc2b2, 0x27d4];
// Simple deterministic string hash, mixed per-row by a seed.
function hash(str, seed){
let h = seed >>> 0;
for (let i = 0; i < str.length; i++){
h = (h ^ str.charCodeAt(i)) >>> 0;
h = Math.imul(h, 0x01000193) >>> 0;
}
h ^= h >>> 15;
return (h >>> 0) % W;
}
function cols(str){ return SEEDS.map(s => hash(str, s)); }
function reset(){
grid = Array.from({length: D}, () => new Array(W).fill(0));
truth = {};
render([]);
setStatus('Add some items, then query.', '');
}
function add(str, n){
const c = cols(str);
for (let t = 0; t < n; t++){
for (let r = 0; r < D; r++) grid[r][c[r]]++;
truth[str] = (truth[str] || 0) + 1;
}
render(c);
}
function estimate(str){
const c = cols(str);
let m = Infinity;
for (let r = 0; r < D; r++) m = Math.min(m, grid[r][c[r]]);
return { est: m, cols: c };
}
function render(active){
const g = document.getElementById('grid');
g.innerHTML = '';
for (let r = 0; r < D; r++){
const lbl = document.createElement('div');
lbl.className = 'lbl'; lbl.textContent = fmt('row %r%%v%', { r: r, v: 1 });
g.appendChild(lbl);
for (let col = 0; col < W; col++){
const d = document.createElement('div');
const v = grid[r][col];
d.className = 'cell' + (v > 0 ? ' hot' : '') + (active[r] === col ? ' pick' : '');
d.textContent = v;
g.appendChild(d);
}
}
}
function setStatus(t, cls){
const s = document.getElementById('status'); s.innerHTML = t; s.className = 'status ' + cls;
}
const POOL = ['apple','banana','cherry','date','fig','grape','kiwi','lemon','mango','pear'];
document.getElementById('add').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (v){ add(v, 1); document.getElementById('qname').textContent = v; }
});
document.getElementById('add5').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (v){ add(v, 5); document.getElementById('qname').textContent = v; }
});
document.getElementById('stream').addEventListener('click', () => {
for (let i = 0; i < 40; i++) add(POOL[Math.floor(Math.random() * POOL.length)], 1);
});
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('item').addEventListener('input', e => {
document.getElementById('qname').textContent = (e.target.value || '').trim() || '?';
});
document.getElementById('q').addEventListener('click', () => {
const v = (document.getElementById('item').value || '').trim();
if (!v){ setStatus('Type an item to query.', ''); return; }
const { est, cols: c } = estimate(v);
render(c);
const real = truth[v] || 0;
const over = est - real;
const note = over === 0 ? 'exactly right — no collisions hit it.'
: fmt('overestimate by %over% (collisions), but never below the truth.', { over: over });
setStatus(fmt('Estimate for "<b>%v%</b>": <b>%est%</b> True count: <b>%real%</b> — %note%', { v: v, est: est, real: real, note: note }), 'ok');
console.log('query', v, '-> estimate', est, '| true', real, '| cells', c.join(','));
});
reset();
Comments
Loading comments...