Choose a dataset size and a RAM (buffer) size, then watch how many disk block transfers each approach needs. Naive sort repeatedly reads single random elements; External merge sort loads full blocks and merges sorted runs — far fewer trips to disk.
<div class="controls">
<label>Data size (N items):
<select id="n-select">
<option value="64">64</option>
<option value="128">128</option>
<option value="256" selected>256</option>
<option value="512">512</option>
<option value="1024">1024</option>
</select>
</label>
<label>RAM size (M items):
<select id="m-select">
<option value="32">32</option>
<option value="64" selected>64</option>
<option value="128">128</option>
</select>
</label>
<label>Block size (B items):
<select id="b-select">
<option value="4">4</option>
<option value="8">8</option>
<option value="16" selected>16</option>
</select>
</label>
<button id="run-btn" type="button">Run both sorts</button>
</div>
<div class="results" id="results">
<div class="algo-box">
<h3>Naive Sort</h3>
<p class="desc">Loads one random element per access — N I/O transfers total.</p>
<div class="bar-wrap"><div class="bar naive" id="naive-bar"></div></div>
<div class="stat" id="naive-stat"></div>
</div>
<div class="algo-box">
<h3>External Merge Sort</h3>
<p class="desc">Loads full B-item blocks; multi-way merges minimize passes.</p>
<div class="bar-wrap"><div class="bar ext" id="ext-bar"></div></div>
<div class="stat" id="ext-stat"></div>
</div>
</div>
<div class="formula" id="formula"></div>
<div class="log" id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin-bottom: 1rem; }
label { display: flex; flex-direction: column; font-size: .83rem; font-weight: 600; gap: .25rem; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; background: #fff; }
button { font: 600 .9rem system-ui; padding: .45rem 1rem; background: #1d3557; color: #fff;
border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #2a4d7d; }
.results { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: .8rem; }
.algo-box { background: #f4f7fa; border: 1px solid #d0dbe6; border-radius: 10px; padding: .8rem 1rem; }
.algo-box h3 { margin: 0 0 .2rem; font-size: .95rem; }
.desc { font-size: .8rem; color: #555; margin: 0 0 .6rem; }
.bar-wrap { background: #dde4ec; border-radius: 4px; height: 18px; overflow: hidden; margin-bottom: .4rem; }
.bar { height: 100%; border-radius: 4px; width: 0; transition: width .5s; }
.bar.naive { background: #e63946; }
.bar.ext { background: #2a9d8f; }
.stat { font-size: .88rem; font-weight: 700; }
.formula { background: #fffbe6; border: 1px solid #e8c84a; border-radius: 8px; padding: .5rem .8rem;
font-size: .83rem; margin-bottom: .6rem; line-height: 1.6; }
.log { font: .78rem ui-monospace, monospace; color: #444; white-space: pre-wrap; line-height: 1.7;
max-height: 130px; overflow: auto; border-top: 1px solid #e0e0e0; padding-top: .4rem; }
@media (max-width: 480px) { .results { grid-template-columns: 1fr; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// External-Memory Algorithm demo
// Cost model (Aggarwal & Vitter, 1988):
// N = total items, B = items per disk block, M = items fitting in RAM (M >> B).
// 1 I/O = load or store one B-item block between disk and RAM.
//
// Naive sort: accesses elements at random positions on disk.
// Worst case: each access hits a different block => N I/Os.
//
// External merge sort:
// Phase 1 — run formation: read all N items in block order, sort each
// M-item chunk in RAM, write back. Cost = 2 * ceil(N/B) I/Os.
// Phase 2 — multi-way merge: merge up to (M/B - 1) runs per pass
// (one input buffer per run + one output buffer, each B items).
// Each pass reads + writes all N items: 2*ceil(N/B) I/Os.
// Passes needed = ceil(log_{fanIn}(numRuns)) where numRuns = ceil(N/M).
// Total = 2*ceil(N/B) * (1 + passes).
// Matches the Aggarwal-Vitter optimal bound O((N/B) log_{M/B}(N/B)).
function ceilDiv(a, b) { return Math.ceil(a / b); }
function naiveIOs(N) {
// N random single-element disk accesses — one I/O per element.
return N;
}
function externalMergeSortIOs(N, M, B) {
const blocksTotal = ceilDiv(N, B);
// Number of M-sized sorted runs after phase 1
const numRuns = ceilDiv(N, M);
if (numRuns <= 1) {
// Fits in RAM: one sequential read + write
return 2 * blocksTotal;
}
// How many runs can we merge simultaneously?
// Need 1 input buffer (B items) per run + 1 output buffer (B items).
// fanIn = floor(M/B) - 1 (reserve 1 buffer for output)
const fanIn = Math.max(2, Math.floor(M / B) - 1);
const mergePasses = Math.ceil(Math.log(numRuns) / Math.log(fanIn));
return 2 * blocksTotal * (1 + mergePasses);
}
function runDemo() {
const N = parseInt(document.getElementById('n-select').value);
const M = parseInt(document.getElementById('m-select').value);
const B = parseInt(document.getElementById('b-select').value);
if (M <= B * 2) {
document.getElementById('log').textContent = 'Please choose M (RAM) at least 3x block size B.';
return;
}
const naiveIO = naiveIOs(N);
const extIO = externalMergeSortIOs(N, M, B);
const maxIO = Math.max(naiveIO, extIO);
// Bars (cap display width so bars fill the container)
document.getElementById('naive-bar').style.width = (naiveIO / maxIO * 100) + '%';
document.getElementById('ext-bar').style.width = (extIO / maxIO * 100) + '%';
const ratio = naiveIO / extIO;
document.getElementById('naive-stat').textContent = fmt('%naiveIO% I/O transfers', { naiveIO: naiveIO.toLocaleString() });
document.getElementById('ext-stat').textContent =
fmt('%extIO% I/O transfers%v%', { extIO: extIO.toLocaleString(), v: ratio > 1 ? ' (' + ratio.toFixed(1) + '× fewer)' : '' });
const numRuns = ceilDiv(N, M);
const fanIn = Math.max(2, Math.floor(M / B) - 1);
const passes = numRuns <= 1 ? 0 : Math.ceil(Math.log(numRuns) / Math.log(fanIn));
const blocksN = ceilDiv(N, B);
document.getElementById('formula').innerHTML =
fmt('<strong>Formula check:</strong> N=%N%, M=%M%, B=%B% → blocks=⌈N/B⌉=%blocksN%, runs=⌈N/M⌉=%numRuns%, fan-in=%fanIn%, merge passes=%passes%. Ext-sort total = 2×%blocksN%×(1+%passes%) = <strong>%extIO% I/Os</strong> vs naive <strong>%naiveIO% I/Os</strong>.', { N: N, M: M, B: B, blocksN: blocksN, numRuns: numRuns, fanIn: fanIn, passes: passes, extIO: extIO, naiveIO: naiveIO });
document.getElementById('log').textContent = [
fmt('[Naive] %N% random accesses × 1 I/O each = %naiveIO% I/Os.', { N: N, naiveIO: naiveIO }),
fmt('[Phase 1] Form %numRuns% sorted run(s): read+write all blocks = 2× 2×%blocksN% = %v% I/Os.', { numRuns: numRuns, blocksN: blocksN, v: 2*blocksN }),
fmt('[Phase 2] %passes% merge pass(es) × 2× 2×%blocksN% = %v% I/Os.', { passes: passes, blocksN: blocksN, v: passes*2*blocksN }),
fmt('[Ext-sort] Total = %extIO% I/Os (Aggarwal-Vitter optimal).', { extIO: extIO }),
ratio > 1
? fmt('[Win] External merge sort uses %ratio%× fewer I/Os than naive.', { ratio: ratio.toFixed(2) })
: '[Note] With these parameters ext-sort I/Os ≥ naive I/Os; try larger N or B.'
].join('\n');
console.log('N=%d M=%d B=%d | naive=%d ext=%d ratio=%.2f', N, M, B, naiveIO, extIO, ratio);
}
document.getElementById('run-btn').addEventListener('click', runDemo);
runDemo();
<div class="controls">
<label>Data size (N items):
<select id="n-select">
<option value="64">64</option>
<option value="128">128</option>
<option value="256" selected>256</option>
<option value="512">512</option>
<option value="1024">1024</option>
</select>
</label>
<label>RAM size (M items):
<select id="m-select">
<option value="32">32</option>
<option value="64" selected>64</option>
<option value="128">128</option>
</select>
</label>
<label>Block size (B items):
<select id="b-select">
<option value="4">4</option>
<option value="8">8</option>
<option value="16" selected>16</option>
</select>
</label>
<button id="run-btn" type="button">Run both sorts</button>
</div>
<div class="results" id="results">
<div class="algo-box">
<h3>Naive Sort</h3>
<p class="desc">Loads one random element per access — N I/O transfers total.</p>
<div class="bar-wrap"><div class="bar naive" id="naive-bar"></div></div>
<div class="stat" id="naive-stat"></div>
</div>
<div class="algo-box">
<h3>External Merge Sort</h3>
<p class="desc">Loads full B-item blocks; multi-way merges minimize passes.</p>
<div class="bar-wrap"><div class="bar ext" id="ext-bar"></div></div>
<div class="stat" id="ext-stat"></div>
</div>
</div>
<div class="formula" id="formula"></div>
<div class="log" id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin-bottom: 1rem; }
label { display: flex; flex-direction: column; font-size: .83rem; font-weight: 600; gap: .25rem; }
select { font-size: .9rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; background: #fff; }
button { font: 600 .9rem system-ui; padding: .45rem 1rem; background: #1d3557; color: #fff;
border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #2a4d7d; }
.results { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: .8rem; }
.algo-box { background: #f4f7fa; border: 1px solid #d0dbe6; border-radius: 10px; padding: .8rem 1rem; }
.algo-box h3 { margin: 0 0 .2rem; font-size: .95rem; }
.desc { font-size: .8rem; color: #555; margin: 0 0 .6rem; }
.bar-wrap { background: #dde4ec; border-radius: 4px; height: 18px; overflow: hidden; margin-bottom: .4rem; }
.bar { height: 100%; border-radius: 4px; width: 0; transition: width .5s; }
.bar.naive { background: #e63946; }
.bar.ext { background: #2a9d8f; }
.stat { font-size: .88rem; font-weight: 700; }
.formula { background: #fffbe6; border: 1px solid #e8c84a; border-radius: 8px; padding: .5rem .8rem;
font-size: .83rem; margin-bottom: .6rem; line-height: 1.6; }
.log { font: .78rem ui-monospace, monospace; color: #444; white-space: pre-wrap; line-height: 1.7;
max-height: 130px; overflow: auto; border-top: 1px solid #e0e0e0; padding-top: .4rem; }
@media (max-width: 480px) { .results { grid-template-columns: 1fr; } }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// External-Memory Algorithm demo
// Cost model (Aggarwal & Vitter, 1988):
// N = total items, B = items per disk block, M = items fitting in RAM (M >> B).
// 1 I/O = load or store one B-item block between disk and RAM.
//
// Naive sort: accesses elements at random positions on disk.
// Worst case: each access hits a different block => N I/Os.
//
// External merge sort:
// Phase 1 — run formation: read all N items in block order, sort each
// M-item chunk in RAM, write back. Cost = 2 * ceil(N/B) I/Os.
// Phase 2 — multi-way merge: merge up to (M/B - 1) runs per pass
// (one input buffer per run + one output buffer, each B items).
// Each pass reads + writes all N items: 2*ceil(N/B) I/Os.
// Passes needed = ceil(log_{fanIn}(numRuns)) where numRuns = ceil(N/M).
// Total = 2*ceil(N/B) * (1 + passes).
// Matches the Aggarwal-Vitter optimal bound O((N/B) log_{M/B}(N/B)).
function ceilDiv(a, b) { return Math.ceil(a / b); }
function naiveIOs(N) {
// N random single-element disk accesses — one I/O per element.
return N;
}
function externalMergeSortIOs(N, M, B) {
const blocksTotal = ceilDiv(N, B);
// Number of M-sized sorted runs after phase 1
const numRuns = ceilDiv(N, M);
if (numRuns <= 1) {
// Fits in RAM: one sequential read + write
return 2 * blocksTotal;
}
// How many runs can we merge simultaneously?
// Need 1 input buffer (B items) per run + 1 output buffer (B items).
// fanIn = floor(M/B) - 1 (reserve 1 buffer for output)
const fanIn = Math.max(2, Math.floor(M / B) - 1);
const mergePasses = Math.ceil(Math.log(numRuns) / Math.log(fanIn));
return 2 * blocksTotal * (1 + mergePasses);
}
function runDemo() {
const N = parseInt(document.getElementById('n-select').value);
const M = parseInt(document.getElementById('m-select').value);
const B = parseInt(document.getElementById('b-select').value);
if (M <= B * 2) {
document.getElementById('log').textContent = 'Please choose M (RAM) at least 3x block size B.';
return;
}
const naiveIO = naiveIOs(N);
const extIO = externalMergeSortIOs(N, M, B);
const maxIO = Math.max(naiveIO, extIO);
// Bars (cap display width so bars fill the container)
document.getElementById('naive-bar').style.width = (naiveIO / maxIO * 100) + '%';
document.getElementById('ext-bar').style.width = (extIO / maxIO * 100) + '%';
const ratio = naiveIO / extIO;
document.getElementById('naive-stat').textContent = fmt('%naiveIO% I/O transfers', { naiveIO: naiveIO.toLocaleString() });
document.getElementById('ext-stat').textContent =
fmt('%extIO% I/O transfers%v%', { extIO: extIO.toLocaleString(), v: ratio > 1 ? ' (' + ratio.toFixed(1) + '× fewer)' : '' });
const numRuns = ceilDiv(N, M);
const fanIn = Math.max(2, Math.floor(M / B) - 1);
const passes = numRuns <= 1 ? 0 : Math.ceil(Math.log(numRuns) / Math.log(fanIn));
const blocksN = ceilDiv(N, B);
document.getElementById('formula').innerHTML =
fmt('<strong>Formula check:</strong> N=%N%, M=%M%, B=%B% → blocks=⌈N/B⌉=%blocksN%, runs=⌈N/M⌉=%numRuns%, fan-in=%fanIn%, merge passes=%passes%. Ext-sort total = 2×%blocksN%×(1+%passes%) = <strong>%extIO% I/Os</strong> vs naive <strong>%naiveIO% I/Os</strong>.', { N: N, M: M, B: B, blocksN: blocksN, numRuns: numRuns, fanIn: fanIn, passes: passes, extIO: extIO, naiveIO: naiveIO });
document.getElementById('log').textContent = [
fmt('[Naive] %N% random accesses × 1 I/O each = %naiveIO% I/Os.', { N: N, naiveIO: naiveIO }),
fmt('[Phase 1] Form %numRuns% sorted run(s): read+write all blocks = 2× 2×%blocksN% = %v% I/Os.', { numRuns: numRuns, blocksN: blocksN, v: 2*blocksN }),
fmt('[Phase 2] %passes% merge pass(es) × 2× 2×%blocksN% = %v% I/Os.', { passes: passes, blocksN: blocksN, v: passes*2*blocksN }),
fmt('[Ext-sort] Total = %extIO% I/Os (Aggarwal-Vitter optimal).', { extIO: extIO }),
ratio > 1
? fmt('[Win] External merge sort uses %ratio%× fewer I/Os than naive.', { ratio: ratio.toFixed(2) })
: '[Note] With these parameters ext-sort I/Os ≥ naive I/Os; try larger N or B.'
].join('\n');
console.log('N=%d M=%d B=%d | naive=%d ext=%d ratio=%.2f', N, M, B, naiveIO, extIO, ratio);
}
document.getElementById('run-btn').addEventListener('click', runDemo);
runDemo();
Notice that as data grows the naive approach's transfer count explodes, while external merge sort's count grows gently as O(BNlogM/BBN) — the theoretical optimum, proven by Aggarwal and Vitter (1988). The demo simulates the I/O pattern; actual bytes are represented by counters so the visualization stays instant.
Comments
Loading comments...