Heaps & Priority Queues
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
min-heap.ts
import type { Tracer } from "../trace";/** The most values one heap holds, so a stage tree stays four levels deep. */export const HEAP_MAX_SIZE = 15;/** Values are small positive integers so every label fits a node. */export const HEAP_MAX_VALUE = 99;/** Operations one script may hold. */export const HEAP_MAX_OPERATIONS = 48;/** One step of a script: push a value, pop the minimum, or build from a batch of values. */export type HeapOperation = | { kind: "push"; value: number } | { kind: "pop" } | { kind: "build"; values: readonly number[] };export interface HeapReport { pushes: number; pops: number; builds: number; /** Every swap made while sifting, up or down. */ swaps: number; /** The values popped, in the order they came out. */ popped: number[]; size: number;}/** * A binary min-heap: a priority queue that always knows its minimum. * The values live in one array read as a complete binary tree, the * children of index i at 2i + 1 and 2i + 2 and its parent at * floor((i - 1) / 2), so the tree costs no pointers and is never * lopsided. One rule holds everywhere: a parent is never larger than * its children, so the minimum is at index 0. A push appends at the * end and sifts the value up while it beats its parent; a pop takes * the root, moves the last value into its place, and sifts it down * toward the smaller child while a child beats it. Both walk one * root-to-leaf path, so they cost the height. Building from n values * sifts each internal node down from the last parent to the root, * which does less work than n pushes because most nodes sit near the * leaves and have almost nothing beneath them. * * Time: O(log n) per push and pop, O(n) to build. Space: O(n), the * array itself. */export class MinHeap { private readonly items: number[] = []; private readonly storage: number[] = []; private swaps = 0; private pushes = 0; private pops = 0; private builds = 0; private readonly popped: number[] = []; constructor(private readonly tracer?: Tracer) {} get size(): number { return this.items.length; return this.storage.length; } /** The minimum, at the root, without removing it. */ peek(): number | undefined { return this.items[0]; return this.storage[0]; } /** The storage array, index 0 the root, for a stage that draws the heap. */ toArray(): number[] { return [...this.items]; return [...this.storage]; } /** The script in order: each push, pop, or build on this one heap. */ run(script: readonly HeapOperation[]): HeapReport { if (script.length > HEAP_MAX_OPERATIONS) throw new RangeError(`${script.length} operations exceeds ${HEAP_MAX_OPERATIONS}`); for (const operation of script) { if (operation.kind === "push") this.push(operation.value); else if (operation.kind === "pop") this.pop(); else this.build(operation.values); } return this.report(); } report(): HeapReport { return { pushes: this.pushes, pops: this.pops, builds: this.builds, swaps: this.swaps, popped: [...this.popped], size: this.items.length }; return { pushes: this.pushes, pops: this.pops, builds: this.builds, swaps: this.swaps, popped: [...this.popped], size: this.storage.length }; } /** Append the value in the next free slot, then sift it up while it is smaller than its parent. */ push(value: number): void { this.checkValue(value); if (this.items.length >= HEAP_MAX_SIZE) throw new RangeError(`the heap holds at most ${HEAP_MAX_SIZE} values`); if (this.storage.length >= HEAP_MAX_SIZE) throw new RangeError(`the heap holds at most ${HEAP_MAX_SIZE} values`); this.pushes++; this.items.push(value); this.storage.push(value); const appended = this.items.length - 1; const freeSlot = this.storage.length - 1; // ▸ a push puts the value in the next free slot, the end of the array this.tracer?.emit("append", { value, index: appended, size: this.items.length }); this.tracer?.emit("append", { value, index: freeSlot, size: this.storage.length }); this.settle(this.siftUp(appended), value); this.settle(this.siftUp(freeSlot), value); } /** Take the root, the minimum; move the last value into its place and sift it down toward the smaller child. */ pop(): number { const top = this.items[0]; const top = this.storage[0]; if (top === undefined) throw new RangeError("pop on an empty heap"); this.pops++; this.popped.push(top); const last = this.items.pop()!; const last = this.storage.pop()!; // ▸ a pop takes the root, the minimum, and frees the last slot this.tracer?.emit("take", { value: top, size: this.items.length }); this.tracer?.emit("take", { value: top, size: this.storage.length }); if (this.items.length === 0) return top; if (this.storage.length === 0) return top; this.items[0] = last; this.storage[0] = last; // ▸ the last value moves into the root, where it is almost surely too large this.tracer?.emit("move-last", { value: last, index: 0 }); this.settle(this.siftDown(0), last); return top; } /** Replace the contents with these values as they come, then sift every internal node down, last parent first. */ build(values: readonly number[]): void { if (values.length > HEAP_MAX_SIZE) throw new RangeError(`the heap holds at most ${HEAP_MAX_SIZE} values`); for (const value of values) this.checkValue(value); this.builds++; this.items.length = 0; this.storage.length = 0; this.items.push(...values); this.storage.push(...values); // ▸ a build takes the values as they come and sifts each internal node down, last parent first this.tracer?.emit("build", { size: this.items.length, values: [...values] }); this.tracer?.emit("build", { size: this.storage.length, values: [...values] }); for (let i = MinHeap.parentOf(this.items.length - 1); i >= 0; i--) { for (let i = MinHeap.parentOf(this.storage.length - 1); i >= 0; i--) { const value = this.items[i]!; const value = this.storage[i]!; this.settle(this.siftDown(i), value); } } /** Where a sifted value came to rest: the one place every sift ends, push, pop, and build alike. */ private settle(index: number, value: number): void { // ▸ the value rests: its parent is no larger and its children no smaller this.tracer?.emit("settle", { index, value }); } /** While the value at i is smaller than its parent, swap them; returns where it rests. */ private siftUp(from: number): number { let i = from; while (i > 0) { const parent = MinHeap.parentOf(i); const childValue = this.items[i]!; const childValue = this.storage[i]!; const parentValue = this.items[parent]!; const parentValue = this.storage[parent]!; if (parentValue <= childValue) break; // ▸ while the value is smaller than its parent, swap them: it climbs one level this.tracer?.emit("swap-up", { child: i, parent, childValue, parentValue }); this.swap(i, parent); i = parent; } return i; } /** While a child is smaller than the value at i, swap it with the smaller child; returns where it rests. */ private siftDown(from: number): number { let i = from; for (;;) { const left = 2 * i + 1; const right = left + 1; let smallest = i; let smallerChild = i; if (left < this.items.length && this.items[left]! < this.items[smallest]!) smallest = left; if (left < this.storage.length && this.storage[left]! < this.storage[smallerChild]!) smallerChild = left; if (right < this.items.length && this.items[right]! < this.items[smallest]!) smallest = right; if (right < this.storage.length && this.storage[right]! < this.storage[smallerChild]!) smallerChild = right; if (smallest === i) break; if (smallerChild === i) break; // ▸ while a child is smaller, swap the value with its smaller child: it sinks one level this.tracer?.emit("swap-down", { parent: i, child: smallest, parentValue: this.items[i]!, childValue: this.items[smallest]! }); this.tracer?.emit("swap-down", { parent: i, child: smallerChild, parentValue: this.storage[i]!, childValue: this.storage[smallerChild]! }); this.swap(i, smallest); this.swap(i, smallerChild); i = smallest; i = smallerChild; } return i; } private swap(a: number, b: number): void { [this.items[a], this.items[b]] = [this.items[b]!, this.items[a]!]; [this.storage[a], this.storage[b]] = [this.storage[b]!, this.storage[a]!]; this.swaps++; } /** The parent of index i; -1 for the root. */ static parentOf(i: number): number { return i <= 0 ? -1 : (i - 1) >> 1; } private checkValue(value: number): void { if (!Number.isInteger(value) || value < 0 || value > HEAP_MAX_VALUE) throw new RangeError(`values must be whole numbers in [0, ${HEAP_MAX_VALUE}]`); }}Script over: 8 pushes, 8 pops, 0 builds, and 9 swaps in all. Popped in order: 2, 7, 24, 41, 47, 52, 70, 97. 0 values left in the heap.