Below is a small shuffled list. Verifying it is sorted is easy — one left-to-right scan, n−1 comparisons. Producing the sorted order is where the cost lives. Run each sorter and watch its comparison counter.
<p class="hint">The same shuffled list, two algorithms. <b>Verifying</b> it is sorted is one cheap left-to-right scan. <b>Producing</b> the order is where the cost lives — and bubble sort pays about n²/2 comparisons while merge sort hugs the n·log₂n wall.</p>
<div class="row">
<label>List size:
<input id="size" type="range" min="4" max="32" value="10">
<span id="sizeval">10</span>
</label>
<button id="shuffle" type="button" class="ghost">New shuffle</button>
</div>
<div id="list" class="list"></div>
<div class="btns">
<button id="verify" type="button">Verify sorted</button>
<button id="bubble" type="button">Run bubble sort</button>
<button id="merge" type="button">Run merge sort</button>
</div>
<div class="stats">
<div class="stat"><span class="k">Verify (scan)</span><span id="vcount" class="v">–</span></div>
<div class="stat"><span class="k">Bubble sort</span><span id="bcount" class="v">–</span></div>
<div class="stat"><span class="k">Merge sort</span><span id="mcount" class="v">–</span></div>
<div class="stat"><span class="k">Wall ≈ n·log₂n</span><span id="wall" class="v">–</span></div>
</div>
<div id="msg" class="status"></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; align-items: center; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 .6rem; font-size: .9rem; }
.row input[type=range] { vertical-align: middle; }
.list { display: flex; gap: 3px; align-items: flex-end; height: 90px; margin: .3rem 0 .7rem; }
.bar { flex: 1; background: #1d3557; border-radius: 3px 3px 0 0; min-width: 4px; }
.bar.hot { background: #e63946; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.stat { display: flex; justify-content: space-between; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .45rem .7rem; }
.stat .k { color: #1d3557; font-size: .85rem; }
.stat .v { font: 700 15px ui-monospace, monospace; color: #0a2540; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The n log n lower bound made visible: bubble sort (~n^2/2 comparisons) vs
// merge sort (~n log2 n), both counting every "is a < b?" question. Verifying a
// sorted list is only n-1 comparisons — checking is cheap, producing is not.
// No comparison sort can drop below ~log2(n!) ~ n log2 n comparisons (decision-tree bound).
let arr = [];
const $ = id => document.getElementById(id);
function shuffle(n){
arr = Array.from({length:n}, (_,i)=>i+1);
for(let i=n-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; }
render(); clearStats();
}
function render(hot){
const box=$('list'); box.innerHTML=''; const max=arr.length||1;
arr.forEach((v,i)=>{ const b=document.createElement('div');
b.className='bar'+(hot&&hot.includes(i)?' hot':''); b.style.height=(8+82*v/max)+'px'; box.appendChild(b); });
}
function clearStats(){ $('vcount').textContent='–'; $('bcount').textContent='–';
$('mcount').textContent='–'; $('wall').textContent='–'; setMsg('',''); }
function setMsg(t,cls){ const m=$('msg'); m.textContent=t; m.className='status '+cls; }
// Verify: a single scan, n-1 comparisons. Cheap.
function verify(){
let cmp=0, ok=true;
for(let i=1;i<arr.length;i++){ cmp++; if(arr[i-1]>arr[i]){ ok=false; break; } }
$('vcount').textContent=cmp;
setMsg(ok ? fmt('✓ Sorted — verified in %cmp% comparisons (one scan).', { cmp: cmp })
: fmt('✗ Not sorted — a scan caught it in %cmp% comparisons.', { cmp: cmp }), ok?'ok':'bad');
}
// Bubble sort: counts comparisons, returns count. ~n^2/2 in the worst case.
function bubbleCount(a){
a=a.slice(); let cmp=0; const n=a.length;
for(let i=0;i<n-1;i++) for(let j=0;j<n-1-i;j++){ cmp++; if(a[j]>a[j+1]){ const t=a[j]; a[j]=a[j+1]; a[j+1]=t; } }
return {cmp, sorted:a};
}
// Merge sort: counts comparisons, returns count. ~n log2 n.
function mergeCount(a){
let cmp=0;
function ms(x){
if(x.length<=1) return x;
const mid=x.length>>1, L=ms(x.slice(0,mid)), R=ms(x.slice(mid)), out=[];
let i=0,j=0;
while(i<L.length && j<R.length){ cmp++; if(L[i]<=R[j]) out.push(L[i++]); else out.push(R[j++]); }
while(i<L.length) out.push(L[i++]);
while(j<R.length) out.push(R[j++]);
return out;
}
const sorted=ms(a); return {cmp, sorted};
}
function runBubble(){ const r=bubbleCount(arr); arr=r.sorted; render();
$('bcount').textContent=r.cmp; showWall(); setMsg(fmt('Bubble sort finished: %cmp% comparisons.', { cmp: r.cmp }),''); }
function runMerge(){ const r=mergeCount(arr); arr=r.sorted; render();
$('mcount').textContent=r.cmp; showWall(); setMsg(fmt('Merge sort finished: %cmp% comparisons.', { cmp: r.cmp }),''); }
function showWall(){ const n=arr.length; const w=Math.ceil(n*Math.log2(Math.max(n,1)));
$('wall').textContent='~'+w; }
$('size').addEventListener('input', e=>{ $('sizeval').textContent=e.target.value; shuffle(+e.target.value); });
$('shuffle').addEventListener('click', ()=>shuffle(+$('size').value));
$('verify').addEventListener('click', verify);
$('bubble').addEventListener('click', runBubble);
$('merge').addEventListener('click', runMerge);
shuffle(10);
<p class="hint">The same shuffled list, two algorithms. <b>Verifying</b> it is sorted is one cheap left-to-right scan. <b>Producing</b> the order is where the cost lives — and bubble sort pays about n²/2 comparisons while merge sort hugs the n·log₂n wall.</p>
<div class="row">
<label>List size:
<input id="size" type="range" min="4" max="32" value="10">
<span id="sizeval">10</span>
</label>
<button id="shuffle" type="button" class="ghost">New shuffle</button>
</div>
<div id="list" class="list"></div>
<div class="btns">
<button id="verify" type="button">Verify sorted</button>
<button id="bubble" type="button">Run bubble sort</button>
<button id="merge" type="button">Run merge sort</button>
</div>
<div class="stats">
<div class="stat"><span class="k">Verify (scan)</span><span id="vcount" class="v">–</span></div>
<div class="stat"><span class="k">Bubble sort</span><span id="bcount" class="v">–</span></div>
<div class="stat"><span class="k">Merge sort</span><span id="mcount" class="v">–</span></div>
<div class="stat"><span class="k">Wall ≈ n·log₂n</span><span id="wall" class="v">–</span></div>
</div>
<div id="msg" class="status"></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; align-items: center; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 .6rem; font-size: .9rem; }
.row input[type=range] { vertical-align: middle; }
.list { display: flex; gap: 3px; align-items: flex-end; height: 90px; margin: .3rem 0 .7rem; }
.bar { flex: 1; background: #1d3557; border-radius: 3px 3px 0 0; min-width: 4px; }
.bar.hot { background: #e63946; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; }
.stat { display: flex; justify-content: space-between; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .45rem .7rem; }
.stat .k { color: #1d3557; font-size: .85rem; }
.stat .v { font: 700 15px ui-monospace, monospace; color: #0a2540; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The n log n lower bound made visible: bubble sort (~n^2/2 comparisons) vs
// merge sort (~n log2 n), both counting every "is a < b?" question. Verifying a
// sorted list is only n-1 comparisons — checking is cheap, producing is not.
// No comparison sort can drop below ~log2(n!) ~ n log2 n comparisons (decision-tree bound).
let arr = [];
const $ = id => document.getElementById(id);
function shuffle(n){
arr = Array.from({length:n}, (_,i)=>i+1);
for(let i=n-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]]; }
render(); clearStats();
}
function render(hot){
const box=$('list'); box.innerHTML=''; const max=arr.length||1;
arr.forEach((v,i)=>{ const b=document.createElement('div');
b.className='bar'+(hot&&hot.includes(i)?' hot':''); b.style.height=(8+82*v/max)+'px'; box.appendChild(b); });
}
function clearStats(){ $('vcount').textContent='–'; $('bcount').textContent='–';
$('mcount').textContent='–'; $('wall').textContent='–'; setMsg('',''); }
function setMsg(t,cls){ const m=$('msg'); m.textContent=t; m.className='status '+cls; }
// Verify: a single scan, n-1 comparisons. Cheap.
function verify(){
let cmp=0, ok=true;
for(let i=1;i<arr.length;i++){ cmp++; if(arr[i-1]>arr[i]){ ok=false; break; } }
$('vcount').textContent=cmp;
setMsg(ok ? fmt('✓ Sorted — verified in %cmp% comparisons (one scan).', { cmp: cmp })
: fmt('✗ Not sorted — a scan caught it in %cmp% comparisons.', { cmp: cmp }), ok?'ok':'bad');
}
// Bubble sort: counts comparisons, returns count. ~n^2/2 in the worst case.
function bubbleCount(a){
a=a.slice(); let cmp=0; const n=a.length;
for(let i=0;i<n-1;i++) for(let j=0;j<n-1-i;j++){ cmp++; if(a[j]>a[j+1]){ const t=a[j]; a[j]=a[j+1]; a[j+1]=t; } }
return {cmp, sorted:a};
}
// Merge sort: counts comparisons, returns count. ~n log2 n.
function mergeCount(a){
let cmp=0;
function ms(x){
if(x.length<=1) return x;
const mid=x.length>>1, L=ms(x.slice(0,mid)), R=ms(x.slice(mid)), out=[];
let i=0,j=0;
while(i<L.length && j<R.length){ cmp++; if(L[i]<=R[j]) out.push(L[i++]); else out.push(R[j++]); }
while(i<L.length) out.push(L[i++]);
while(j<R.length) out.push(R[j++]);
return out;
}
const sorted=ms(a); return {cmp, sorted};
}
function runBubble(){ const r=bubbleCount(arr); arr=r.sorted; render();
$('bcount').textContent=r.cmp; showWall(); setMsg(fmt('Bubble sort finished: %cmp% comparisons.', { cmp: r.cmp }),''); }
function runMerge(){ const r=mergeCount(arr); arr=r.sorted; render();
$('mcount').textContent=r.cmp; showWall(); setMsg(fmt('Merge sort finished: %cmp% comparisons.', { cmp: r.cmp }),''); }
function showWall(){ const n=arr.length; const w=Math.ceil(n*Math.log2(Math.max(n,1)));
$('wall').textContent='~'+w; }
$('size').addEventListener('input', e=>{ $('sizeval').textContent=e.target.value; shuffle(+e.target.value); });
$('shuffle').addEventListener('click', ()=>shuffle(+$('size').value));
$('verify').addEventListener('click', verify);
$('bubble').addEventListener('click', runBubble);
$('merge').addEventListener('click', runMerge);
shuffle(10);
Bubble sort wastes effort — it pays about n2/2 comparisons. Merge sort is disciplined and pays about nlogn. Push the size up and watch the gap explode: the smart algorithm hugs the wall, the naive one races away from it. No comparison sort ever drops below that merge-sort line — that floor is the whole story.
Comments
Loading comments...