Below is a live min-heap shown two ways at once: as the underlying array and as the tree that array encodes. Type a number and press Insert — watch it climb (sift up) until no parent is larger. Press Extract min to pull the smallest off the front and watch the gap close (sift down).
<p class="hint">A <b>min-heap</b> is just an array where every parent is no larger than its children (child of <i>i</i> at <i>2i+1</i> and <i>2i+2</i>). Insert a number to watch it <b>sift up</b>; extract the minimum to watch the gap <b>sift down</b>. The smallest value is always at index 0.</p>
<div class="controls">
<input id="val" type="number" value="42" />
<button id="insert" type="button">Insert</button>
<button id="extract" type="button">Extract min</button>
<button id="rand" type="button" class="ghost">Random</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="status" id="status">Heap is empty. Insert a number.</div>
<div class="label">Tree view</div>
<div id="tree" class="tree"></div>
<div class="label">Array (index : value)</div>
<div id="array" class="array"></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; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .4rem; }
input { font: 600 14px system-ui, sans-serif; width: 80px; padding: .4rem .5rem;
border: 1px solid #adb1b8; border-radius: 8px; }
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; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.3em; color: #1d3557; }
.status.ok { color: #0a7d33; }
.label { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #6b7785;
margin: .7rem 0 .25rem; font-weight: 700; }
.tree { position: relative; height: 200px; border: 1px solid #e1e6eb; border-radius: 10px;
background: #f7f9fb; overflow: hidden; }
.node { position: absolute; width: 38px; height: 38px; margin: -19px 0 0 -19px;
display: flex; align-items: center; justify-content: center; border-radius: 50%;
background: #1d3557; color: #fff; font: 700 14px ui-monospace, monospace;
transition: left .25s, top .25s, background .25s; }
.node.root { background: #e63946; }
.edge { position: absolute; height: 2px; background: #c2cdd8; transform-origin: 0 0; }
.array { display: flex; flex-wrap: wrap; gap: 4px; }
.cell { min-width: 42px; padding: .35rem; border: 1px solid #cdd9e3; border-radius: 7px;
background: #e8eef3; text-align: center; font: 700 14px ui-monospace, monospace; color: #1d3557; }
.cell .idx { display: block; font-size: 10px; color: #6b7785; font-weight: 600; }
.cell.first { background: #e63946; color: #fff; border-color: #c92f3c; }
.cell.first .idx { color: #ffd9dd; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A binary min-heap stored as a flat array `h`. Parent of i = (i-1>>1);
// children = 2i+1, 2i+2. Invariant: h[parent] <= h[child] everywhere.
// insert(): append, then sift up. extractMin(): take h[0], move last to front,
// sift down. Both touch one leaf<->root path => O(log n).
const h = [];
function swap(i, j){ const t = h[i]; h[i] = h[j]; h[j] = t; }
function siftUp(i){
while (i > 0){
const p = (i - 1) >> 1;
if (h[p] <= h[i]) break;
swap(i, p); i = p;
}
}
function siftDown(i){
const n = h.length;
while (true){
let s = i, l = 2*i+1, r = 2*i+2;
if (l < n && h[l] < h[s]) s = l;
if (r < n && h[r] < h[s]) s = r;
if (s === i) break;
swap(i, s); i = s;
}
}
function insert(v){ h.push(v); siftUp(h.length - 1); }
function extractMin(){
if (h.length === 0) return null;
const min = h[0];
const last = h.pop();
if (h.length > 0){ h[0] = last; siftDown(0); }
return min;
}
function setStatus(t, ok){ const s = document.getElementById('status');
s.textContent = t; s.className = 'status' + (ok ? ' ok' : ''); }
function render(){
// Array view
const a = document.getElementById('array'); a.innerHTML = '';
if (h.length === 0){ a.innerHTML = '<span style="color:#6b7785;font-size:.9rem">(empty)</span>'; }
h.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell' + (i === 0 ? ' first' : '');
c.innerHTML = '<span class="idx">' + i + '</span>' + v;
a.appendChild(c);
});
// Tree view: place node i at depth = floor(log2(i+1)), spread across its level.
const tree = document.getElementById('tree'); tree.innerHTML = '';
const W = tree.clientWidth || 520, levelH = 48, topPad = 24;
const pos = (i) => {
const depth = Math.floor(Math.log2(i + 1));
const first = (1 << depth) - 1; // first index on this level
const slot = i - first; // position within level
const count = 1 << depth; // nodes on a full level
const x = W * (slot + 0.5) / count;
const y = topPad + depth * levelH;
return { x, y };
};
// edges first
for (let i = 1; i < h.length; i++){
const p = (i - 1) >> 1;
const a1 = pos(p), b1 = pos(i);
const dx = b1.x - a1.x, dy = b1.y - a1.y;
const len = Math.sqrt(dx*dx + dy*dy);
const ang = Math.atan2(dy, dx) * 180 / Math.PI;
const e = document.createElement('div'); e.className = 'edge';
e.style.left = a1.x + 'px'; e.style.top = a1.y + 'px';
e.style.width = len + 'px'; e.style.transform = 'rotate(' + ang + 'deg)';
tree.appendChild(e);
}
// nodes
h.forEach((v, i) => {
const p = pos(i);
const n = document.createElement('div');
n.className = 'node' + (i === 0 ? ' root' : '');
n.style.left = p.x + 'px'; n.style.top = p.y + 'px';
n.textContent = v;
tree.appendChild(n);
});
}
document.getElementById('insert').addEventListener('click', () => {
const raw = document.getElementById('val').value;
const v = parseInt(raw, 10);
if (Number.isNaN(v)){ setStatus('Type a whole number first.', false); return; }
insert(v); render();
setStatus(fmt('Inserted %v%. Smallest is now %v2% (index 0).', { v: v, v2: h[0] }), true);
});
document.getElementById('extract').addEventListener('click', () => {
if (h.length === 0){ setStatus('Heap is empty — nothing to extract.', false); return; }
const m = extractMin(); render();
setStatus(fmt('Extracted minimum %m%.%v%', { m: m, v: h.length ? ' New smallest: ' + h[0] + '.' : ' Heap is now empty.' }), true);
});
document.getElementById('rand').addEventListener('click', () => {
const v = Math.floor(Math.random() * 99) + 1;
document.getElementById('val').value = v;
insert(v); render();
setStatus(fmt('Inserted random %v%. Smallest is now %v2%.', { v: v, v2: h[0] }), true);
});
document.getElementById('reset').addEventListener('click', () => {
h.length = 0; render(); setStatus('Heap cleared. Insert a number.', false);
});
// Seed a small heap so the view is not empty on load.
[42, 17, 63, 8, 25, 50].forEach(insert);
render();
setStatus(fmt('Seeded heap. Smallest is %v% at index 0.', { v: h[0] }), true);
<p class="hint">A <b>min-heap</b> is just an array where every parent is no larger than its children (child of <i>i</i> at <i>2i+1</i> and <i>2i+2</i>). Insert a number to watch it <b>sift up</b>; extract the minimum to watch the gap <b>sift down</b>. The smallest value is always at index 0.</p>
<div class="controls">
<input id="val" type="number" value="42" />
<button id="insert" type="button">Insert</button>
<button id="extract" type="button">Extract min</button>
<button id="rand" type="button" class="ghost">Random</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div class="status" id="status">Heap is empty. Insert a number.</div>
<div class="label">Tree view</div>
<div id="tree" class="tree"></div>
<div class="label">Array (index : value)</div>
<div id="array" class="array"></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; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .4rem; }
input { font: 600 14px system-ui, sans-serif; width: 80px; padding: .4rem .5rem;
border: 1px solid #adb1b8; border-radius: 8px; }
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; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.3em; color: #1d3557; }
.status.ok { color: #0a7d33; }
.label { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #6b7785;
margin: .7rem 0 .25rem; font-weight: 700; }
.tree { position: relative; height: 200px; border: 1px solid #e1e6eb; border-radius: 10px;
background: #f7f9fb; overflow: hidden; }
.node { position: absolute; width: 38px; height: 38px; margin: -19px 0 0 -19px;
display: flex; align-items: center; justify-content: center; border-radius: 50%;
background: #1d3557; color: #fff; font: 700 14px ui-monospace, monospace;
transition: left .25s, top .25s, background .25s; }
.node.root { background: #e63946; }
.edge { position: absolute; height: 2px; background: #c2cdd8; transform-origin: 0 0; }
.array { display: flex; flex-wrap: wrap; gap: 4px; }
.cell { min-width: 42px; padding: .35rem; border: 1px solid #cdd9e3; border-radius: 7px;
background: #e8eef3; text-align: center; font: 700 14px ui-monospace, monospace; color: #1d3557; }
.cell .idx { display: block; font-size: 10px; color: #6b7785; font-weight: 600; }
.cell.first { background: #e63946; color: #fff; border-color: #c92f3c; }
.cell.first .idx { color: #ffd9dd; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A binary min-heap stored as a flat array `h`. Parent of i = (i-1>>1);
// children = 2i+1, 2i+2. Invariant: h[parent] <= h[child] everywhere.
// insert(): append, then sift up. extractMin(): take h[0], move last to front,
// sift down. Both touch one leaf<->root path => O(log n).
const h = [];
function swap(i, j){ const t = h[i]; h[i] = h[j]; h[j] = t; }
function siftUp(i){
while (i > 0){
const p = (i - 1) >> 1;
if (h[p] <= h[i]) break;
swap(i, p); i = p;
}
}
function siftDown(i){
const n = h.length;
while (true){
let s = i, l = 2*i+1, r = 2*i+2;
if (l < n && h[l] < h[s]) s = l;
if (r < n && h[r] < h[s]) s = r;
if (s === i) break;
swap(i, s); i = s;
}
}
function insert(v){ h.push(v); siftUp(h.length - 1); }
function extractMin(){
if (h.length === 0) return null;
const min = h[0];
const last = h.pop();
if (h.length > 0){ h[0] = last; siftDown(0); }
return min;
}
function setStatus(t, ok){ const s = document.getElementById('status');
s.textContent = t; s.className = 'status' + (ok ? ' ok' : ''); }
function render(){
// Array view
const a = document.getElementById('array'); a.innerHTML = '';
if (h.length === 0){ a.innerHTML = '<span style="color:#6b7785;font-size:.9rem">(empty)</span>'; }
h.forEach((v, i) => {
const c = document.createElement('div');
c.className = 'cell' + (i === 0 ? ' first' : '');
c.innerHTML = '<span class="idx">' + i + '</span>' + v;
a.appendChild(c);
});
// Tree view: place node i at depth = floor(log2(i+1)), spread across its level.
const tree = document.getElementById('tree'); tree.innerHTML = '';
const W = tree.clientWidth || 520, levelH = 48, topPad = 24;
const pos = (i) => {
const depth = Math.floor(Math.log2(i + 1));
const first = (1 << depth) - 1; // first index on this level
const slot = i - first; // position within level
const count = 1 << depth; // nodes on a full level
const x = W * (slot + 0.5) / count;
const y = topPad + depth * levelH;
return { x, y };
};
// edges first
for (let i = 1; i < h.length; i++){
const p = (i - 1) >> 1;
const a1 = pos(p), b1 = pos(i);
const dx = b1.x - a1.x, dy = b1.y - a1.y;
const len = Math.sqrt(dx*dx + dy*dy);
const ang = Math.atan2(dy, dx) * 180 / Math.PI;
const e = document.createElement('div'); e.className = 'edge';
e.style.left = a1.x + 'px'; e.style.top = a1.y + 'px';
e.style.width = len + 'px'; e.style.transform = 'rotate(' + ang + 'deg)';
tree.appendChild(e);
}
// nodes
h.forEach((v, i) => {
const p = pos(i);
const n = document.createElement('div');
n.className = 'node' + (i === 0 ? ' root' : '');
n.style.left = p.x + 'px'; n.style.top = p.y + 'px';
n.textContent = v;
tree.appendChild(n);
});
}
document.getElementById('insert').addEventListener('click', () => {
const raw = document.getElementById('val').value;
const v = parseInt(raw, 10);
if (Number.isNaN(v)){ setStatus('Type a whole number first.', false); return; }
insert(v); render();
setStatus(fmt('Inserted %v%. Smallest is now %v2% (index 0).', { v: v, v2: h[0] }), true);
});
document.getElementById('extract').addEventListener('click', () => {
if (h.length === 0){ setStatus('Heap is empty — nothing to extract.', false); return; }
const m = extractMin(); render();
setStatus(fmt('Extracted minimum %m%.%v%', { m: m, v: h.length ? ' New smallest: ' + h[0] + '.' : ' Heap is now empty.' }), true);
});
document.getElementById('rand').addEventListener('click', () => {
const v = Math.floor(Math.random() * 99) + 1;
document.getElementById('val').value = v;
insert(v); render();
setStatus(fmt('Inserted random %v%. Smallest is now %v2%.', { v: v, v2: h[0] }), true);
});
document.getElementById('reset').addEventListener('click', () => {
h.length = 0; render(); setStatus('Heap cleared. Insert a number.', false);
});
// Seed a small heap so the view is not empty on load.
[42, 17, 63, 8, 25, 50].forEach(insert);
render();
setStatus(fmt('Seeded heap. Smallest is %v% at index 0.', { v: h[0] }), true);
Notice the discipline. The heap is never fully sorted, yet the smallest element is always at index 0. Each insert or extract only touches one path from a leaf to the root — about log2 n swaps — so even with a million elements that is only twenty steps. Keep pressing Extract min and the numbers come out in perfect sorted order: that is exactly how heapsort works.
Comments
Loading comments...