CookbookLabs and workshopsNo. 28
An intro programming courseunits, worked examples and sorting you can watch
See it in know.sh
Four ways to sort sixteen bars
Bubble, insertion, selection and merge sort on the same sixteen numbers, one comparison or one move per step, with running counts. Insertion sort here swaps neighbours rather than shifting them; merge sort is top-down and copies into a second list, so it counts writes, not swaps, and the bars show the merged part followed by what is left of each half. Time, memory and the computer’s caches are not modelled.
<div class="sorter" tabindex="-1">
<div class="algos" role="group" aria-label="Algorithm">
<button type="button" data-algo="bubble" aria-pressed="true">Bubble</button>
<button type="button" data-algo="insertion" aria-pressed="false">Insertion</button>
<button type="button" data-algo="selection" aria-pressed="false">Selection</button>
<button type="button" data-algo="merge" aria-pressed="false">Merge</button>
</div>
<div class="stage">
<div class="bars" id="bars" role="img" aria-label="Bars"></div>
</div>
<p class="legend" aria-hidden="true"><span><i class="key k-cmp"></i>Compared</span><span><i class="key k-mov"></i><span id="movName">Swapped</span></span><span><i class="key k-done"></i>In place</span><span><i class="key k-rng"></i>Working on</span></p>
<dl class="counts">
<div><dt>Comparisons</dt><dd id="cOut">0</dd></div>
<div><dt id="sName">Swaps</dt><dd id="sOut">0</dd></div>
<div><dt>Step</dt><dd id="kOut">0</dd></div>
</dl>
<p class="say" id="say" aria-live="polite"></p>
<div class="controls">
<button type="button" id="step">Step</button>
<button type="button" id="play" aria-pressed="false">Play</button>
<button type="button" id="reset" class="quiet">Reset</button>
<button type="button" id="shuffle" class="quiet">New order</button>
<label class="speed" for="speed">Speed <input id="speed" type="range" min="1" max="5" step="1" value="3"> <output id="speedOut" for="speed">4 steps a second</output></label>
</div>
</div>.sorter { outline: none; }
.algos { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.algos button, .controls .quiet { background: var(--paper); color: var(--ink); border: 1px solid var(--ink); }
.algos button { padding: 8px 4px; min-height: 40px; }
.algos button[aria-pressed='true'] { background: var(--ink); color: var(--paper); }
.stage { margin-top: 14px; border-bottom: 1px solid var(--ink); }
.bars { display: grid; grid-template-columns: repeat(16, minmax(0, 1fr)); grid-template-rows: 170px auto 10px; column-gap: 3px; }
.bar { grid-row: 1; align-self: end; box-sizing: border-box; background: var(--wash); border: 1px solid var(--ink-3); border-bottom: 0; }
.bar.done { background: var(--ink-3); border-color: var(--ink-3); }
.bar.cmp { background: var(--ink); border-color: var(--ink); }
.bar.mov { border-color: var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 3px, var(--paper) 3px 6px); }
.val { grid-row: 2; padding-top: 4px; text-align: center; font: 400 11px/1 var(--sans); color: var(--ink-3); font-variant-numeric: tabular-nums; }
.val.on { color: var(--ink); font-weight: 600; }
.rng { grid-row: 3; align-self: start; height: 6px; margin-top: 3px; border: 1px solid var(--ink-2); border-top: 0; }
.legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 10px 0 0; font: 400 12px/1.4 var(--sans); color: var(--ink-2); }
.legend > span { display: inline-flex; align-items: center; gap: 6px; }
.key { display: inline-block; width: 12px; height: 12px; box-sizing: border-box; }
.k-cmp { background: var(--ink); }
.k-mov { border: 1px solid var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 2px, var(--paper) 2px 4px); }
.k-done { background: var(--ink-3); }
.k-rng { height: 6px; border: 1px solid var(--ink-2); border-top: 0; }
.counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }
.counts div { padding: 7px 0; border-bottom: 1px solid var(--rule); }
.counts dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }
.counts dd { margin: 2px 0 0; font: 400 22px/1.2 var(--serif); font-variant-numeric: tabular-nums; }
.say { min-height: 3em; margin: 10px 0 0; font: italic 400 16px/1.45 var(--serif); }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule); }
.controls button { min-height: 40px; min-width: 64px; }
.speed { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; }
.speed input { flex: 1; min-width: 80px; }
.speed output { min-width: 8.5em; font: 400 13px var(--sans); color: var(--ink-2); font-variant-numeric: tabular-nums; }
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (max-width: 420px) {
.algos { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bars { column-gap: 2px; grid-template-rows: 150px auto 10px; }
.val { font-size: 10px; }
.controls button { flex: 1 1 40%; }
}(function () {
var root = document.querySelector('.sorter');
var $ = function (id) { return document.getElementById(id); };
var N = 16, LOW = 4, HIGH = 48;
var SPEEDS = [1, 2, 4, 8, 16];
var seed = 20260927, order = [], steps = [], at = 0, algo = 'bubble';
var playing = false, raf = 0, lastT = 0;
var barsEl = $('bars'), bars = [], vals = [], rng;
// A seeded Park-Miller generator, so the page opens on the same order every time.
function nextOrder() {
var pool = [], out = [];
for (var v = LOW; v <= HIGH; v++) pool.push(v);
for (var i = 0; i < N; i++) {
seed = (seed * 16807) % 2147483647;
out.push(pool.splice(seed % pool.length, 1)[0]);
}
return out;
}
function inversions(a) {
var n = 0;
for (var i = 0; i < a.length; i++) for (var j = i + 1; j < a.length; j++) if (a[i] > a[j]) n++;
return n;
}
// Runs the algorithm to the end and records every comparison and every move.
function trace(kind, input) {
var a = input.slice(), out = [], c = 0, s = 0, done = [], range = null, i, j;
for (i = 0; i < N; i++) done.push(false);
function push(o) {
out.push({ a: a.slice(), cmp: o.cmp || [], mov: o.mov || [], rng: range ? range.slice() : null, done: done.slice(), c: c, s: s, msg: o.msg });
}
function swap(x, y) { var t = a[x]; a[x] = a[y]; a[y] = t; s++; }
function allDone() { for (var k = 0; k < N; k++) done[k] = true; range = null; }
var inv = inversions(a);
push({ msg: 'The starting order: ' + N + ' bars, ' + inv + ' pairs out of order. Press Step or Play.' });
if (kind === 'bubble') {
var end = N - 1, pass = 1;
while (end > 0) {
range = [0, end];
var last = 0;
for (i = 0; i < end; i++) {
c++;
var out1 = a[i] > a[i + 1];
push({ cmp: [i, i + 1], msg: 'Pass ' + pass + ': compare ' + a[i] + ' and ' + a[i + 1] + (out1 ? '. Out of order.' : '. In order; leave them.') });
if (out1) { swap(i, i + 1); last = i; push({ mov: [i, i + 1], msg: 'Swap them: ' + a[i + 1] + ' moves right.' }); }
}
for (var k = last + 1; k <= end; k++) done[k] = true;
end = last;
if (end === 0) done[0] = true;
push({ msg: end > 0 ? 'End of pass ' + pass + '. Nothing after the last swap moves again, so those bars are in place.' : 'End of pass ' + pass + ': no swaps left to make.' });
pass++;
}
allDone();
push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Each swap put one out-of-order pair right, and the start had ' + inv + '.' });
}
if (kind === 'insertion') {
for (i = 1; i < N; i++) {
range = [0, i];
push({ msg: 'Take ' + a[i] + ' and walk it left into the sorted part.' });
for (j = i; j > 0; j--) {
c++;
var gt = a[j - 1] > a[j];
push({ cmp: [j - 1, j], msg: 'Compare ' + a[j - 1] + ' and ' + a[j] + (gt ? ': ' + a[j] + ' is smaller, so it moves left.' : ': in order, so ' + a[j] + ' stops here.') });
if (!gt) break;
swap(j - 1, j);
push({ mov: [j - 1, j], msg: 'Swap them.' });
}
}
allDone();
push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. The swaps equal the ' + inv + ' out-of-order pairs; the comparisons are at most one more per bar.' });
}
if (kind === 'selection') {
for (i = 0; i < N - 1; i++) {
range = [i, N - 1];
var m = i;
for (j = i + 1; j < N; j++) {
c++;
var smaller = a[j] < a[m];
push({ cmp: [m, j], msg: 'Compare ' + a[j] + ' with the smallest so far, ' + a[m] + (smaller ? ': a new smallest.' : '.') });
if (smaller) m = j;
}
if (m !== i) {
swap(i, m); done[i] = true;
push({ mov: [i, m], msg: 'Swap the smallest left, ' + a[i] + ', into position ' + (i + 1) + '.' });
} else {
done[i] = true;
push({ msg: a[i] + ' is already the smallest left, so no swap.' });
}
}
allDone();
push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Selection sort always makes 120 comparisons on 16 bars, and never more than 15 swaps.' });
}
if (kind === 'merge') {
var list = function (x) { return x.join(', '); };
var merge = function (lo, hi) {
if (lo >= hi) return;
var mid = Math.floor((lo + hi) / 2);
merge(lo, mid);
merge(mid + 1, hi);
range = [lo, hi];
var L = a.slice(lo, mid + 1), R = a.slice(mid + 1, hi + 1), p = 0, q = 0, k = lo;
push({ msg: 'Merge ' + list(L) + ' with ' + list(R) + '.' });
while (p < L.length && q < R.length) {
c++;
var rh = k + (L.length - p);
push({ cmp: [k, rh], msg: 'Compare the fronts of the two halves, ' + L[p] + ' and ' + R[q] + '.' });
if (L[p] <= R[q]) { p++; }
else { a.splice(rh, 1); a.splice(k, 0, R[q]); q++; }
s++;
push({ mov: [k], msg: 'Write ' + a[k] + ', the smaller, next.' });
k++;
}
if (k <= hi) {
var rest = [];
for (var t = k; t <= hi; t++) rest.push(t);
s += rest.length;
push({ mov: rest, msg: 'One half is used up; write the other ' + rest.length + ' as they stand.' });
}
};
merge(0, N - 1);
allDone();
push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' writes: four rounds of merging, 16 writes each. No swaps: merge sort copies into a second list.' });
}
return out;
}
function build() {
barsEl.textContent = '';
bars = []; vals = [];
for (var i = 0; i < N; i++) {
var b = document.createElement('div'); b.className = 'bar'; b.style.gridColumn = String(i + 1);
var v = document.createElement('div'); v.className = 'val'; v.style.gridColumn = String(i + 1);
barsEl.appendChild(b); barsEl.appendChild(v);
bars.push(b); vals.push(v);
}
rng = document.createElement('div'); rng.className = 'rng';
barsEl.appendChild(rng);
}
function render() {
var st = steps[at];
for (var i = 0; i < N; i++) {
var on = st.cmp.indexOf(i) >= 0, mv = st.mov.indexOf(i) >= 0;
bars[i].style.height = Math.round(st.a[i] / HIGH * 100) + '%';
bars[i].className = 'bar' + (st.done[i] ? ' done' : '') + (on ? ' cmp' : '') + (mv ? ' mov' : '');
vals[i].textContent = st.a[i];
vals[i].className = 'val' + (on || mv ? ' on' : '');
}
if (st.rng) { rng.style.display = ''; rng.style.gridColumn = (st.rng[0] + 1) + ' / ' + (st.rng[1] + 2); }
else rng.style.display = 'none';
barsEl.setAttribute('aria-label', 'Bars from left to right: ' + st.a.join(', '));
$('cOut').textContent = st.c;
$('sOut').textContent = st.s;
$('kOut').textContent = at + ' of ' + (steps.length - 1);
$('say').textContent = st.msg;
var merge = algo === 'merge';
$('sName').textContent = merge ? 'Writes' : 'Swaps';
$('movName').textContent = merge ? 'Written' : 'Swapped';
$('step').disabled = at >= steps.length - 1;
}
function load() {
stop();
steps = trace(algo, order);
at = 0;
render();
}
function advance() {
if (at < steps.length - 1) { at++; render(); }
}
function loop(t) {
if (!playing) return;
var interval = 1000 / SPEEDS[Number($('speed').value) - 1];
if (!lastT) lastT = t;
if (t - lastT >= interval) { lastT = t; advance(); }
if (at >= steps.length - 1) { stop(); return; }
raf = requestAnimationFrame(loop);
}
function play() {
if (at >= steps.length - 1) { at = 0; render(); }
playing = true; lastT = 0;
$('play').textContent = 'Pause'; $('play').setAttribute('aria-pressed', 'true');
$('say').setAttribute('aria-live', 'off');
raf = requestAnimationFrame(loop);
}
function stop() {
playing = false;
if (raf) cancelAnimationFrame(raf);
raf = 0;
$('play').textContent = 'Play'; $('play').setAttribute('aria-pressed', 'false');
$('say').setAttribute('aria-live', 'polite');
}
function speedText() {
var n = SPEEDS[Number($('speed').value) - 1];
var t = n + (n === 1 ? ' step a second' : ' steps a second');
$('speedOut').textContent = t;
$('speed').setAttribute('aria-valuetext', t);
}
root.querySelectorAll('[data-algo]').forEach(function (b) {
b.addEventListener('click', function () {
algo = b.getAttribute('data-algo');
root.querySelectorAll('[data-algo]').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });
load();
});
});
$('step').addEventListener('click', function () { stop(); advance(); });
$('play').addEventListener('click', function () { if (playing) stop(); else play(); });
$('reset').addEventListener('click', load);
$('shuffle').addEventListener('click', function () { order = nextOrder(); load(); });
$('speed').addEventListener('input', speedText);
build();
order = nextOrder();
speedText();
load();
})();```element widget
{
"title": "Four ways to sort sixteen bars",
"caption": "Bubble, insertion, selection and merge sort on the same sixteen numbers, one comparison or one move per step, with running counts. Insertion sort here swaps neighbours rather than shifting them; merge sort is top-down and copies into a second list, so it counts writes, not swaps, and the bars show the merged part followed by what is left of each half. Time, memory and the computer’s caches are not modelled.",
"height": 545,
"html": "<div class=\"sorter\" tabindex=\"-1\">\n <div class=\"algos\" role=\"group\" aria-label=\"Algorithm\">\n <button type=\"button\" data-algo=\"bubble\" aria-pressed=\"true\">Bubble</button>\n <button type=\"button\" data-algo=\"insertion\" aria-pressed=\"false\">Insertion</button>\n <button type=\"button\" data-algo=\"selection\" aria-pressed=\"false\">Selection</button>\n <button type=\"button\" data-algo=\"merge\" aria-pressed=\"false\">Merge</button>\n </div>\n <div class=\"stage\">\n <div class=\"bars\" id=\"bars\" role=\"img\" aria-label=\"Bars\"></div>\n </div>\n <p class=\"legend\" aria-hidden=\"true\"><span><i class=\"key k-cmp\"></i>Compared</span><span><i class=\"key k-mov\"></i><span id=\"movName\">Swapped</span></span><span><i class=\"key k-done\"></i>In place</span><span><i class=\"key k-rng\"></i>Working on</span></p>\n <dl class=\"counts\">\n <div><dt>Comparisons</dt><dd id=\"cOut\">0</dd></div>\n <div><dt id=\"sName\">Swaps</dt><dd id=\"sOut\">0</dd></div>\n <div><dt>Step</dt><dd id=\"kOut\">0</dd></div>\n </dl>\n <p class=\"say\" id=\"say\" aria-live=\"polite\"></p>\n <div class=\"controls\">\n <button type=\"button\" id=\"step\">Step</button>\n <button type=\"button\" id=\"play\" aria-pressed=\"false\">Play</button>\n <button type=\"button\" id=\"reset\" class=\"quiet\">Reset</button>\n <button type=\"button\" id=\"shuffle\" class=\"quiet\">New order</button>\n <label class=\"speed\" for=\"speed\">Speed <input id=\"speed\" type=\"range\" min=\"1\" max=\"5\" step=\"1\" value=\"3\"> <output id=\"speedOut\" for=\"speed\">4 steps a second</output></label>\n </div>\n</div>",
"css": ".sorter { outline: none; }\n.algos { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }\n.algos button, .controls .quiet { background: var(--paper); color: var(--ink); border: 1px solid var(--ink); }\n.algos button { padding: 8px 4px; min-height: 40px; }\n.algos button[aria-pressed='true'] { background: var(--ink); color: var(--paper); }\n.stage { margin-top: 14px; border-bottom: 1px solid var(--ink); }\n.bars { display: grid; grid-template-columns: repeat(16, minmax(0, 1fr)); grid-template-rows: 170px auto 10px; column-gap: 3px; }\n.bar { grid-row: 1; align-self: end; box-sizing: border-box; background: var(--wash); border: 1px solid var(--ink-3); border-bottom: 0; }\n.bar.done { background: var(--ink-3); border-color: var(--ink-3); }\n.bar.cmp { background: var(--ink); border-color: var(--ink); }\n.bar.mov { border-color: var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 3px, var(--paper) 3px 6px); }\n.val { grid-row: 2; padding-top: 4px; text-align: center; font: 400 11px/1 var(--sans); color: var(--ink-3); font-variant-numeric: tabular-nums; }\n.val.on { color: var(--ink); font-weight: 600; }\n.rng { grid-row: 3; align-self: start; height: 6px; margin-top: 3px; border: 1px solid var(--ink-2); border-top: 0; }\n.legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 10px 0 0; font: 400 12px/1.4 var(--sans); color: var(--ink-2); }\n.legend > span { display: inline-flex; align-items: center; gap: 6px; }\n.key { display: inline-block; width: 12px; height: 12px; box-sizing: border-box; }\n.k-cmp { background: var(--ink); }\n.k-mov { border: 1px solid var(--ink); background: repeating-linear-gradient(135deg, var(--ink) 0 2px, var(--paper) 2px 4px); }\n.k-done { background: var(--ink-3); }\n.k-rng { height: 6px; border: 1px solid var(--ink-2); border-top: 0; }\n.counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }\n.counts div { padding: 7px 0; border-bottom: 1px solid var(--rule); }\n.counts dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }\n.counts dd { margin: 2px 0 0; font: 400 22px/1.2 var(--serif); font-variant-numeric: tabular-nums; }\n.say { min-height: 3em; margin: 10px 0 0; font: italic 400 16px/1.45 var(--serif); }\n.controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule); }\n.controls button { min-height: 40px; min-width: 64px; }\n.speed { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; }\n.speed input { flex: 1; min-width: 80px; }\n.speed output { min-width: 8.5em; font: 400 13px var(--sans); color: var(--ink-2); font-variant-numeric: tabular-nums; }\nbutton:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }\n@media (max-width: 420px) {\n .algos { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .bars { column-gap: 2px; grid-template-rows: 150px auto 10px; }\n .val { font-size: 10px; }\n .controls button { flex: 1 1 40%; }\n}",
"js": "(function () {\n var root = document.querySelector('.sorter');\n var $ = function (id) { return document.getElementById(id); };\n var N = 16, LOW = 4, HIGH = 48;\n var SPEEDS = [1, 2, 4, 8, 16];\n var seed = 20260927, order = [], steps = [], at = 0, algo = 'bubble';\n var playing = false, raf = 0, lastT = 0;\n var barsEl = $('bars'), bars = [], vals = [], rng;\n\n // A seeded Park-Miller generator, so the page opens on the same order every time.\n function nextOrder() {\n var pool = [], out = [];\n for (var v = LOW; v <= HIGH; v++) pool.push(v);\n for (var i = 0; i < N; i++) {\n seed = (seed * 16807) % 2147483647;\n out.push(pool.splice(seed % pool.length, 1)[0]);\n }\n return out;\n }\n\n function inversions(a) {\n var n = 0;\n for (var i = 0; i < a.length; i++) for (var j = i + 1; j < a.length; j++) if (a[i] > a[j]) n++;\n return n;\n }\n\n // Runs the algorithm to the end and records every comparison and every move.\n function trace(kind, input) {\n var a = input.slice(), out = [], c = 0, s = 0, done = [], range = null, i, j;\n for (i = 0; i < N; i++) done.push(false);\n function push(o) {\n out.push({ a: a.slice(), cmp: o.cmp || [], mov: o.mov || [], rng: range ? range.slice() : null, done: done.slice(), c: c, s: s, msg: o.msg });\n }\n function swap(x, y) { var t = a[x]; a[x] = a[y]; a[y] = t; s++; }\n function allDone() { for (var k = 0; k < N; k++) done[k] = true; range = null; }\n var inv = inversions(a);\n push({ msg: 'The starting order: ' + N + ' bars, ' + inv + ' pairs out of order. Press Step or Play.' });\n\n if (kind === 'bubble') {\n var end = N - 1, pass = 1;\n while (end > 0) {\n range = [0, end];\n var last = 0;\n for (i = 0; i < end; i++) {\n c++;\n var out1 = a[i] > a[i + 1];\n push({ cmp: [i, i + 1], msg: 'Pass ' + pass + ': compare ' + a[i] + ' and ' + a[i + 1] + (out1 ? '. Out of order.' : '. In order; leave them.') });\n if (out1) { swap(i, i + 1); last = i; push({ mov: [i, i + 1], msg: 'Swap them: ' + a[i + 1] + ' moves right.' }); }\n }\n for (var k = last + 1; k <= end; k++) done[k] = true;\n end = last;\n if (end === 0) done[0] = true;\n push({ msg: end > 0 ? 'End of pass ' + pass + '. Nothing after the last swap moves again, so those bars are in place.' : 'End of pass ' + pass + ': no swaps left to make.' });\n pass++;\n }\n allDone();\n push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Each swap put one out-of-order pair right, and the start had ' + inv + '.' });\n }\n\n if (kind === 'insertion') {\n for (i = 1; i < N; i++) {\n range = [0, i];\n push({ msg: 'Take ' + a[i] + ' and walk it left into the sorted part.' });\n for (j = i; j > 0; j--) {\n c++;\n var gt = a[j - 1] > a[j];\n push({ cmp: [j - 1, j], msg: 'Compare ' + a[j - 1] + ' and ' + a[j] + (gt ? ': ' + a[j] + ' is smaller, so it moves left.' : ': in order, so ' + a[j] + ' stops here.') });\n if (!gt) break;\n swap(j - 1, j);\n push({ mov: [j - 1, j], msg: 'Swap them.' });\n }\n }\n allDone();\n push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. The swaps equal the ' + inv + ' out-of-order pairs; the comparisons are at most one more per bar.' });\n }\n\n if (kind === 'selection') {\n for (i = 0; i < N - 1; i++) {\n range = [i, N - 1];\n var m = i;\n for (j = i + 1; j < N; j++) {\n c++;\n var smaller = a[j] < a[m];\n push({ cmp: [m, j], msg: 'Compare ' + a[j] + ' with the smallest so far, ' + a[m] + (smaller ? ': a new smallest.' : '.') });\n if (smaller) m = j;\n }\n if (m !== i) {\n swap(i, m); done[i] = true;\n push({ mov: [i, m], msg: 'Swap the smallest left, ' + a[i] + ', into position ' + (i + 1) + '.' });\n } else {\n done[i] = true;\n push({ msg: a[i] + ' is already the smallest left, so no swap.' });\n }\n }\n allDone();\n push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' swaps. Selection sort always makes 120 comparisons on 16 bars, and never more than 15 swaps.' });\n }\n\n if (kind === 'merge') {\n var list = function (x) { return x.join(', '); };\n var merge = function (lo, hi) {\n if (lo >= hi) return;\n var mid = Math.floor((lo + hi) / 2);\n merge(lo, mid);\n merge(mid + 1, hi);\n range = [lo, hi];\n var L = a.slice(lo, mid + 1), R = a.slice(mid + 1, hi + 1), p = 0, q = 0, k = lo;\n push({ msg: 'Merge ' + list(L) + ' with ' + list(R) + '.' });\n while (p < L.length && q < R.length) {\n c++;\n var rh = k + (L.length - p);\n push({ cmp: [k, rh], msg: 'Compare the fronts of the two halves, ' + L[p] + ' and ' + R[q] + '.' });\n if (L[p] <= R[q]) { p++; }\n else { a.splice(rh, 1); a.splice(k, 0, R[q]); q++; }\n s++;\n push({ mov: [k], msg: 'Write ' + a[k] + ', the smaller, next.' });\n k++;\n }\n if (k <= hi) {\n var rest = [];\n for (var t = k; t <= hi; t++) rest.push(t);\n s += rest.length;\n push({ mov: rest, msg: 'One half is used up; write the other ' + rest.length + ' as they stand.' });\n }\n };\n merge(0, N - 1);\n allDone();\n push({ msg: 'Sorted in ' + c + ' comparisons and ' + s + ' writes: four rounds of merging, 16 writes each. No swaps: merge sort copies into a second list.' });\n }\n return out;\n }\n\n function build() {\n barsEl.textContent = '';\n bars = []; vals = [];\n for (var i = 0; i < N; i++) {\n var b = document.createElement('div'); b.className = 'bar'; b.style.gridColumn = String(i + 1);\n var v = document.createElement('div'); v.className = 'val'; v.style.gridColumn = String(i + 1);\n barsEl.appendChild(b); barsEl.appendChild(v);\n bars.push(b); vals.push(v);\n }\n rng = document.createElement('div'); rng.className = 'rng';\n barsEl.appendChild(rng);\n }\n\n function render() {\n var st = steps[at];\n for (var i = 0; i < N; i++) {\n var on = st.cmp.indexOf(i) >= 0, mv = st.mov.indexOf(i) >= 0;\n bars[i].style.height = Math.round(st.a[i] / HIGH * 100) + '%';\n bars[i].className = 'bar' + (st.done[i] ? ' done' : '') + (on ? ' cmp' : '') + (mv ? ' mov' : '');\n vals[i].textContent = st.a[i];\n vals[i].className = 'val' + (on || mv ? ' on' : '');\n }\n if (st.rng) { rng.style.display = ''; rng.style.gridColumn = (st.rng[0] + 1) + ' / ' + (st.rng[1] + 2); }\n else rng.style.display = 'none';\n barsEl.setAttribute('aria-label', 'Bars from left to right: ' + st.a.join(', '));\n $('cOut').textContent = st.c;\n $('sOut').textContent = st.s;\n $('kOut').textContent = at + ' of ' + (steps.length - 1);\n $('say').textContent = st.msg;\n var merge = algo === 'merge';\n $('sName').textContent = merge ? 'Writes' : 'Swaps';\n $('movName').textContent = merge ? 'Written' : 'Swapped';\n $('step').disabled = at >= steps.length - 1;\n }\n\n function load() {\n stop();\n steps = trace(algo, order);\n at = 0;\n render();\n }\n\n function advance() {\n if (at < steps.length - 1) { at++; render(); }\n }\n\n function loop(t) {\n if (!playing) return;\n var interval = 1000 / SPEEDS[Number($('speed').value) - 1];\n if (!lastT) lastT = t;\n if (t - lastT >= interval) { lastT = t; advance(); }\n if (at >= steps.length - 1) { stop(); return; }\n raf = requestAnimationFrame(loop);\n }\n\n function play() {\n if (at >= steps.length - 1) { at = 0; render(); }\n playing = true; lastT = 0;\n $('play').textContent = 'Pause'; $('play').setAttribute('aria-pressed', 'true');\n $('say').setAttribute('aria-live', 'off');\n raf = requestAnimationFrame(loop);\n }\n\n function stop() {\n playing = false;\n if (raf) cancelAnimationFrame(raf);\n raf = 0;\n $('play').textContent = 'Play'; $('play').setAttribute('aria-pressed', 'false');\n $('say').setAttribute('aria-live', 'polite');\n }\n\n function speedText() {\n var n = SPEEDS[Number($('speed').value) - 1];\n var t = n + (n === 1 ? ' step a second' : ' steps a second');\n $('speedOut').textContent = t;\n $('speed').setAttribute('aria-valuetext', t);\n }\n\n root.querySelectorAll('[data-algo]').forEach(function (b) {\n b.addEventListener('click', function () {\n algo = b.getAttribute('data-algo');\n root.querySelectorAll('[data-algo]').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });\n load();\n });\n });\n $('step').addEventListener('click', function () { stop(); advance(); });\n $('play').addEventListener('click', function () { if (playing) stop(); else play(); });\n $('reset').addEventListener('click', load);\n $('shuffle').addEventListener('click', function () { order = nextOrder(); load(); });\n $('speed').addEventListener('input', speedText);\n\n build();\n order = nextOrder();\n speedText();\n load();\n})();"
}
```Intro to programmingNo. 5
Unit 5 — Searching and sorting
14 sections, 4,020 words, about 17 minutes, filed 30 August, revised 22 September, 5 highlights.
Two weeks. By the end, students can trace linear and binary search by hand, write insertion sort from memory, and say why merge sort beats it on a long list and loses to it on a short, nearly sorted one. Every example runs on Python 3.12; the exercise files are in the class repository, unit-05.
The unit’s sorting algorithms, side by side
| Algorithm | Best | Average | Worst | Extra memory | Stable |
|---|---|---|---|---|---|
| Bubble sort, stopping early | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Selection sort | O(n²) | O(n²) | O(n²) | O(1) | No |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| ✓Python’s sorted() | O(n) | O(n log n) | O(n log n) | O(n) | Yes |
Comparisons made on a list of n items. Bubble sort reaches O(n) only if it stops after a pass with no swaps; insertion sort does on a list already in order. Selection sort as usually written can swap equal items out of their order, so it is not stable. Python’s sorted() is Timsort, a merge sort that finds runs already in order and extends short ones by insertion: marked as the one to use outside this unit.
The unit’s sorting algorithms, side by side
| Algorithm | Best | Average | Worst | Extra memory | Stable |
|---|---|---|---|---|---|
| Bubble sort, stopping early | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Selection sort | O(n²) | O(n²) | O(n²) | O(1) | No |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| ✓ Python’s sorted() | O(n) | O(n log n) | O(n log n) | O(n) | Yes |
Comparisons made on a list of n items. Bubble sort reaches O(n) only if it stops after a pass with no swaps; insertion sort does on a list already in order. Selection sort as usually written can swap equal items out of their order, so it is not stable. Python’s sorted() is Timsort, a merge sort that finds runs already in order and extends short ones by insertion: marked as the one to use outside this unit.
```element table
{
"caption": "The unit’s sorting algorithms, side by side",
"columns": [
{
"label": "Algorithm",
"align": "left"
},
{
"label": "Best",
"align": "left"
},
{
"label": "Average",
"align": "left"
},
{
"label": "Worst",
"align": "left"
},
{
"label": "Extra memory",
"align": "left"
},
{
"label": "Stable",
"align": "left"
}
],
"rows": [
[
"Bubble sort, stopping early",
"O(n)",
"O(n²)",
"O(n²)",
"O(1)",
"Yes"
],
[
"Insertion sort",
"O(n)",
"O(n²)",
"O(n²)",
"O(1)",
"Yes"
],
[
"Selection sort",
"O(n²)",
"O(n²)",
"O(n²)",
"O(1)",
"No"
],
[
"Merge sort",
"O(n log n)",
"O(n log n)",
"O(n log n)",
"O(n)",
"Yes"
],
[
"Python’s sorted()",
"O(n)",
"O(n log n)",
"O(n log n)",
"O(n)",
"Yes"
]
],
"pick": 4,
"notes": "Comparisons made on a list of *n* items. Bubble sort reaches O(n) only if it stops after a pass with no swaps; insertion sort does on a list already in order. Selection sort as usually written can swap equal items out of their order, so it is not stable. Python’s `sorted()` is Timsort, a merge sort that finds runs already in order and extends short ones by insertion: marked as the one to use outside this unit."
}
```Sections 3 to 6 each hold one algorithm, with its code, a trace and a Bug or two; the visualiser is in section 3.
Sections
- 1Linear searchInsight
- 2Binary search, and why the list must be sortedInsight, key
- 3Insertion sortInsight, key, 2 code blocksTake the next item and walk it left through the sorted part until the item before it is not larger.
- 4Selection and bubble sortInsight
- 5Merge sortInsight, key
- 6My list came back as NoneBug, key
items.sort()sorts in place and returnsNone;sorted(items)returns a new list.
and eight more sections
Intro to programmingUnit 5 — Searching and sorting
3of 14
Insertion sort
Insight, key section, 2 code blocks, 1 highlight, 1 note, 310 words
The idea. Take the next item and walk it left through the sorted part, swapping as you go, until the item before it is not larger. It is how most people sort a hand of cards.
def insertion_sort(items):
"""Sort a list in place, smallest first."""
for i in range(1, len(items)):
j = i
while j > 0 and items[j - 1] > items[j]:
items[j - 1], items[j] = items[j], items[j - 1]
j -= 1
Worked example. insertion_sort([5, 2, 4, 1]), one line per pass:
start [5, 2, 4, 1]
i = 1, take 2 [2, 5, 4, 1] 1 comparison, 1 swap
i = 2, take 4 [2, 4, 5, 1] 2 comparisons, 1 swap
i = 3, take 1 [1, 2, 4, 5] 3 comparisons, 3 swaps
Why it matters. Six comparisons and five swaps, and exactly one swap for every pair that started out of order: (5, 2), (5, 4), (5, 1), (2, 1) and (4, 1). On a list already in order it makes one comparison per item and no swaps at all, which is why Python’s own sort uses a form of insertion sort on short runs.
Watch it. Open the visualiser under this section, choose Insertion, and press Step: the swaps counter always ends on the number of pairs the starting order had out of order.
Ran on Python 3.12, 20 September.
How it works
Put your intro course in one place, mistakes included. Each unit is a document: a section per concept with a worked example and a trace, a section per exercise, and every mistake your classes make filed as a Bug. Beside the sorting unit sits a visualiser that counts every comparison and swap as the bars move.