Below, items numbered 1, 2, 3, … stream past a reservoir that holds just k of them. The rule is almost suspiciously simple: when item number i arrives, keep it with probability k / i, and if you keep it, drop one of the items already in the reservoir at random.
<p class="hint">A stream of items <b>1, 2, 3, …</b> flows past a reservoir of size <b>k = 1</b>. When item <b>i</b> arrives it is kept with probability <b>k/i</b>; if kept, it evicts a random item already inside. Stream items, or run thousands of full streams and watch every position get chosen <b>equally often</b>.</p>
<div class="reservoir">
<span class="lbl">Reservoir (k=1):</span>
<span id="held" class="held">—</span>
<span class="lbl">next item:</span>
<span id="next" class="next">1</span>
</div>
<div class="bars" id="bars"></div>
<div class="status" id="status">Stream items one by one, or run many trials.</div>
<div class="btns">
<button id="one" type="button">Stream one</button>
<button id="many" type="button">Run many trials</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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; }
.reservoir { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .4rem 0 .8rem; }
.lbl { font-size: .85rem; color: #555; }
.held, .next { width: 40px; height: 40px; display: inline-flex; align-items: center;
justify-content: center; font: 700 16px ui-monospace, monospace; border-radius: 8px; }
.held { background: #1d3557; color: #fff; }
.next { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.bars { display: flex; align-items: flex-end; gap: 4px; height: 130px; margin: .6rem 0;
border-bottom: 2px solid #ccc; padding-bottom: 2px; }
.col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 3px; }
.bar { width: 100%; background: #457b9d; border-radius: 4px 4px 0 0; transition: height .15s; min-height: 1px; }
.bar.lit { background: #e63946; }
.tag { font: 600 11px ui-monospace, monospace; color: #555; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #0a4d68; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Reservoir sampling, Algorithm R (Vitter, 1985), with reservoir size k = 1.
// Stream length here is N items. When item i (1-based) arrives, keep it with
// probability k/i; if kept, it replaces a random item in the reservoir.
// Claim: every position 1..N ends up the chosen one with probability k/N = 1/N.
const N = 8; // items in one stream pass
const K = 1; // reservoir size
let i = 0; // items seen so far in the current live pass
let held = null; // current reservoir contents (single item, since K=1)
const counts = new Array(N).fill(0); // how often each position is the survivor
let trials = 0;
// One streaming decision for the live, step-by-step view.
function step(){
if (i >= N){ setStatus('Stream finished. Reset to replay, or run many trials.'); return; }
i++;
// keep item i with probability k/i
if (Math.random() < K / i) held = i;
render();
if (i === N){ counts[held-1]++; trials++; setStatus(fmt('Survivor of this pass: item %held%.', { held: held })); }
else setStatus(fmt('Item %i% arrived (kept with prob %K%/%i%).', { i: i, K: K }));
}
// Run one full stream silently and return the surviving position.
function onePass(){
let h = null;
for (let t = 1; t <= N; t++){ if (Math.random() < K / t) h = t; }
return h;
}
function runMany(){
for (let t = 0; t < 20000; t++){ counts[onePass()-1]++; trials++; }
held = null; i = 0;
render();
const exp = (trials / N).toFixed(0);
setStatus(fmt('%trials% trials. Each position expected ~%Number% times — see how level the bars are.', { trials: trials.toLocaleString(), Number: Number(exp).toLocaleString() }));
}
function render(){
document.getElementById('held').textContent = held === null ? '—' : held;
document.getElementById('next').textContent = i >= N ? '—' : (i + 1);
const max = Math.max(1, ...counts);
const bars = document.getElementById('bars'); bars.innerHTML = '';
for (let p = 0; p < N; p++){
const col = document.createElement('div'); col.className = 'col';
const bar = document.createElement('div');
bar.className = 'bar' + (held === p + 1 ? ' lit' : '');
bar.style.height = (counts[p] / max * 110 + 1) + 'px';
const tag = document.createElement('div'); tag.className = 'tag'; tag.textContent = p + 1;
col.appendChild(bar); col.appendChild(tag); bars.appendChild(col);
}
}
function setStatus(t){ document.getElementById('status').textContent = t; }
function reset(){ i = 0; held = null; render(); setStatus('Stream items one by one, or run many trials.'); }
document.getElementById('one').addEventListener('click', step);
document.getElementById('many').addEventListener('click', runMany);
document.getElementById('reset').addEventListener('click', reset);
render();
<p class="hint">A stream of items <b>1, 2, 3, …</b> flows past a reservoir of size <b>k = 1</b>. When item <b>i</b> arrives it is kept with probability <b>k/i</b>; if kept, it evicts a random item already inside. Stream items, or run thousands of full streams and watch every position get chosen <b>equally often</b>.</p>
<div class="reservoir">
<span class="lbl">Reservoir (k=1):</span>
<span id="held" class="held">—</span>
<span class="lbl">next item:</span>
<span id="next" class="next">1</span>
</div>
<div class="bars" id="bars"></div>
<div class="status" id="status">Stream items one by one, or run many trials.</div>
<div class="btns">
<button id="one" type="button">Stream one</button>
<button id="many" type="button">Run many trials</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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; }
.reservoir { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: .4rem 0 .8rem; }
.lbl { font-size: .85rem; color: #555; }
.held, .next { width: 40px; height: 40px; display: inline-flex; align-items: center;
justify-content: center; font: 700 16px ui-monospace, monospace; border-radius: 8px; }
.held { background: #1d3557; color: #fff; }
.next { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.bars { display: flex; align-items: flex-end; gap: 4px; height: 130px; margin: .6rem 0;
border-bottom: 2px solid #ccc; padding-bottom: 2px; }
.col { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 3px; }
.bar { width: 100%; background: #457b9d; border-radius: 4px 4px 0 0; transition: height .15s; min-height: 1px; }
.bar.lit { background: #e63946; }
.tag { font: 600 11px ui-monospace, monospace; color: #555; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #0a4d68; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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] ?? ''));
// Reservoir sampling, Algorithm R (Vitter, 1985), with reservoir size k = 1.
// Stream length here is N items. When item i (1-based) arrives, keep it with
// probability k/i; if kept, it replaces a random item in the reservoir.
// Claim: every position 1..N ends up the chosen one with probability k/N = 1/N.
const N = 8; // items in one stream pass
const K = 1; // reservoir size
let i = 0; // items seen so far in the current live pass
let held = null; // current reservoir contents (single item, since K=1)
const counts = new Array(N).fill(0); // how often each position is the survivor
let trials = 0;
// One streaming decision for the live, step-by-step view.
function step(){
if (i >= N){ setStatus('Stream finished. Reset to replay, or run many trials.'); return; }
i++;
// keep item i with probability k/i
if (Math.random() < K / i) held = i;
render();
if (i === N){ counts[held-1]++; trials++; setStatus(fmt('Survivor of this pass: item %held%.', { held: held })); }
else setStatus(fmt('Item %i% arrived (kept with prob %K%/%i%).', { i: i, K: K }));
}
// Run one full stream silently and return the surviving position.
function onePass(){
let h = null;
for (let t = 1; t <= N; t++){ if (Math.random() < K / t) h = t; }
return h;
}
function runMany(){
for (let t = 0; t < 20000; t++){ counts[onePass()-1]++; trials++; }
held = null; i = 0;
render();
const exp = (trials / N).toFixed(0);
setStatus(fmt('%trials% trials. Each position expected ~%Number% times — see how level the bars are.', { trials: trials.toLocaleString(), Number: Number(exp).toLocaleString() }));
}
function render(){
document.getElementById('held').textContent = held === null ? '—' : held;
document.getElementById('next').textContent = i >= N ? '—' : (i + 1);
const max = Math.max(1, ...counts);
const bars = document.getElementById('bars'); bars.innerHTML = '';
for (let p = 0; p < N; p++){
const col = document.createElement('div'); col.className = 'col';
const bar = document.createElement('div');
bar.className = 'bar' + (held === p + 1 ? ' lit' : '');
bar.style.height = (counts[p] / max * 110 + 1) + 'px';
const tag = document.createElement('div'); tag.className = 'tag'; tag.textContent = p + 1;
col.appendChild(bar); col.appendChild(tag); bars.appendChild(col);
}
}
function setStatus(t){ document.getElementById('status').textContent = t; }
function reset(){ i = 0; held = null; render(); setStatus('Stream items one by one, or run many trials.'); }
document.getElementById('one').addEventListener('click', step);
document.getElementById('many').addEventListener('click', runMany);
document.getElementById('reset').addEventListener('click', reset);
render();
Press Stream one to feed items in one at a time, or Run many trials to repeat the whole stream thousands of times and tally how often each position survives. Notice the punchline: even though early items had many chances to be evicted, every position ends up in the final sample equally often. One pass, a fixed-size reservoir, and perfectly uniform odds.
Comments
Loading comments...