diff --git a/data_structures/deno.json b/data_structures/deno.json index da895e9caadc..8e1e0ca97be1 100644 --- a/data_structures/deno.json +++ b/data_structures/deno.json @@ -11,6 +11,7 @@ "./red-black-tree": "./red_black_tree.ts", "./unstable-2d-array": "./unstable_2d_array.ts", "./unstable-rolling-counter": "./unstable_rolling_counter.ts", - "./unstable-deque": "./unstable_deque.ts" + "./unstable-deque": "./unstable_deque.ts", + "./unstable-indexed-heap": "./unstable_indexed_heap.ts" } } diff --git a/data_structures/unstable_indexed_heap.ts b/data_structures/unstable_indexed_heap.ts new file mode 100644 index 000000000000..88293b5f9817 --- /dev/null +++ b/data_structures/unstable_indexed_heap.ts @@ -0,0 +1,639 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** Allows the class to mutate priority internally. */ +interface MutableEntry { + readonly key: K; + priority: number; +} + +/** + * A key-priority pair returned by {@linkcode IndexedHeap} methods. + * + * Fields are `readonly` to signal that mutating a returned entry has no + * effect on the heap. + * + * @typeParam K The type of the key. + */ +export interface HeapEntry { + /** The key that identifies this entry in the heap. */ + readonly key: K; + /** The numeric priority of this entry (smaller = higher priority). */ + readonly priority: number; +} + +/** + * Read-only view of an {@linkcode IndexedHeap}. Exposes only query methods + * (`peek`, `has`, `getPriority`, `size`, `isEmpty`), hiding all methods + * that modify the heap. Follows the same pattern as `ReadonlyMap` and + * `ReadonlySet`. + * + * Note: `[Symbol.iterator]` is intentionally excluded because the heap's + * iterator is destructive (it drains all entries). + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @typeParam K The type of the keys in the heap. + */ +export type ReadonlyIndexedHeap = Pick< + IndexedHeap, + | "peek" + | "peekKey" + | "peekPriority" + | "has" + | "getPriority" + | "size" + | "isEmpty" +>; + +/** Throws if the priority is NaN, which would silently corrupt the heap. */ +function assertValidPriority(priority: number): void { + if (Number.isNaN(priority)) { + throw new RangeError("Cannot set priority: value is NaN"); + } +} + +/** + * A priority queue that supports looking up, removing, and re-prioritizing + * entries by key. Each entry is a unique `(key, priority)` pair. The entry + * with the smallest priority is always at the front. + * + * Unlike {@linkcode BinaryHeap}, which only allows popping the top element, + * `IndexedHeap` lets you delete or update any entry by its key in + * logarithmic time. + * + * Priorities are plain numbers, always sorted smallest-first. To sort + * largest-first instead, negate the priorities. + * + * | Method | Time complexity | + * | --------------------- | -------------------------------- | + * | peek() | Constant | + * | peekKey() | Constant | + * | peekPriority() | Constant | + * | pop() | Logarithmic in the number of entries | + * | push(key, priority) | Logarithmic in the number of entries | + * | delete(key) | Logarithmic in the number of entries | + * | update(key, priority) | Logarithmic in the number of entries | + * | has(key) | Constant | + * | getPriority(key) | Constant | + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 3); + * heap.push("b", 1); + * heap.push("c", 2); + * + * assertEquals(heap.peek(), { key: "b", priority: 1 }); + * assertEquals(heap.pop(), { key: "b", priority: 1 }); + * assertEquals([...heap], [{ key: "c", priority: 2 }, { key: "a", priority: 3 }]); + * ``` + * + * @typeParam K The type of the keys in the heap. Keys are compared the + * same way as `Map` keys — by reference for objects, by value for + * primitives. + */ +export class IndexedHeap implements Iterable> { + #data: MutableEntry[] = []; + #index: Map = new Map(); + + /** + * A string tag for the class, used by `Object.prototype.toString()`. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * assertEquals(heap[Symbol.toStringTag], "IndexedHeap"); + * ``` + */ + readonly [Symbol.toStringTag] = "IndexedHeap" as const; + + /** Bubble the entry at `pos` up toward the root while it is smaller than its parent. */ + #siftUp(pos: number): number { + const data = this.#data; + const index = this.#index; + const entry = data[pos]!; + const priority = entry.priority; + while (pos > 0) { + const parentPos = (pos - 1) >>> 1; + const parent = data[parentPos]!; + if (priority < parent.priority) { + data[pos] = parent; + index.set(parent.key, pos); + pos = parentPos; + } else { + break; + } + } + data[pos] = entry; + index.set(entry.key, pos); + return pos; + } + + /** Bubble the entry at `pos` down while a child is smaller. */ + #siftDown(pos: number): void { + const data = this.#data; + const index = this.#index; + const size = data.length; + const entry = data[pos]!; + const priority = entry.priority; + while (true) { + const left = 2 * pos + 1; + if (left >= size) break; + const right = left + 1; + let childPos = left; + let childPri = data[left]!.priority; + if (right < size) { + const rp = data[right]!.priority; + if (rp < childPri) { + childPos = right; + childPri = rp; + } + } + if (childPri < priority) { + const child = data[childPos]!; + data[pos] = child; + index.set(child.key, pos); + pos = childPos; + } else { + break; + } + } + data[pos] = entry; + index.set(entry.key, pos); + } + + /** + * Insert a new key with the given priority. Throws if the key already + * exists — use {@linkcode IndexedHeap.prototype.update | update} or + * {@linkcode IndexedHeap.prototype.pushOrUpdate | pushOrUpdate} instead. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("task-1", 10); + * assertEquals(heap.size, 1); + * assertEquals(heap.peek(), { key: "task-1", priority: 10 }); + * ``` + * + * @param key The key to insert. + * @param priority The numeric priority (smaller = higher priority). + */ + push(key: K, priority: number): void { + assertValidPriority(priority); + if (this.#index.has(key)) { + throw new Error( + `Cannot push into IndexedHeap: key already exists`, + ); + } + const pos = this.#data.length; + this.#data.push({ key, priority }); + this.#siftUp(pos); + } + + /** + * Remove and return the front entry (smallest priority), or `undefined` + * if the heap is empty. The returned entry is removed from the heap so + * the caller owns it. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 2); + * heap.push("b", 1); + * + * assertEquals(heap.pop(), { key: "b", priority: 1 }); + * assertEquals(heap.pop(), { key: "a", priority: 2 }); + * assertEquals(heap.pop(), undefined); + * ``` + * + * @returns The front entry, or `undefined` if empty. + */ + pop(): HeapEntry | undefined { + const size = this.#data.length; + if (size === 0) return undefined; + + const root = this.#data[0]!; + this.#index.delete(root.key); + + if (size === 1) { + this.#data.pop(); + return root; + } + + const last = this.#data.pop()!; + this.#data[0] = last; + this.#siftDown(0); + return root; + } + + /** + * Return the front entry (smallest priority) without removing it, or + * `undefined` if the heap is empty. + * + * The returned object is a copy; mutating it does not affect the heap. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("x", 5); + * heap.push("y", 3); + * + * assertEquals(heap.peek(), { key: "y", priority: 3 }); + * assertEquals(heap.size, 2); + * ``` + * + * @returns A copy of the front entry, or `undefined` if empty. + */ + peek(): HeapEntry | undefined { + const entry = this.#data[0]; + if (entry === undefined) return undefined; + return { key: entry.key, priority: entry.priority }; + } + + /** + * Return the key of the front entry (smallest priority), or `undefined` + * if the heap is empty. Unlike + * {@linkcode IndexedHeap.prototype.peek | peek}, does not allocate a + * wrapper object. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("x", 5); + * heap.push("y", 3); + * + * assertEquals(heap.peekKey(), "y"); + * assertEquals(heap.size, 2); + * ``` + * + * @returns The key of the front entry, or `undefined` if empty. + */ + peekKey(): K | undefined { + return this.#data[0]?.key; + } + + /** + * Return the priority of the front entry (smallest priority), or + * `undefined` if the heap is empty. Unlike + * {@linkcode IndexedHeap.prototype.peek | peek}, does not allocate a + * wrapper object. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("x", 5); + * heap.push("y", 3); + * + * assertEquals(heap.peekPriority(), 3); + * assertEquals(heap.size, 2); + * ``` + * + * @returns The priority of the front entry, or `undefined` if empty. + */ + peekPriority(): number | undefined { + return this.#data[0]?.priority; + } + + /** + * Remove the entry with the given key. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 1); + * heap.push("b", 2); + * + * assertEquals(heap.delete("a"), true); + * assertEquals(heap.delete("z"), false); + * assertEquals(heap.size, 1); + * ``` + * + * @param key The key to remove. + * @returns `true` if the key was present, `false` otherwise. + */ + delete(key: K): boolean { + const pos = this.#index.get(key); + if (pos === undefined) return false; + + this.#index.delete(key); + const lastIndex = this.#data.length - 1; + + if (pos === lastIndex) { + this.#data.pop(); + return true; + } + + const last = this.#data.pop()!; + this.#data[pos] = last; + + const afterUp = this.#siftUp(pos); + if (afterUp === pos) { + this.#siftDown(pos); + } + return true; + } + + /** + * Change the priority of an existing key. Throws if the key is not + * present — use {@linkcode IndexedHeap.prototype.push | push} or + * {@linkcode IndexedHeap.prototype.pushOrUpdate | pushOrUpdate} instead. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 10); + * heap.push("b", 20); + * + * heap.update("b", 1); + * assertEquals(heap.peek(), { key: "b", priority: 1 }); + * ``` + * + * @param key The key whose priority to change. + * @param priority The new priority. + */ + update(key: K, priority: number): void { + assertValidPriority(priority); + const pos = this.#index.get(key); + if (pos === undefined) { + throw new Error( + `Cannot update IndexedHeap: key does not exist`, + ); + } + this.#data[pos]!.priority = priority; + const afterUp = this.#siftUp(pos); + if (afterUp === pos) { + this.#siftDown(pos); + } + } + + /** + * Insert the key if absent, or update its priority if present. This is a + * convenience method combining + * {@linkcode IndexedHeap.prototype.push | push} and + * {@linkcode IndexedHeap.prototype.update | update}. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.pushOrUpdate("a", 10); + * assertEquals(heap.getPriority("a"), 10); + * + * heap.pushOrUpdate("a", 5); + * assertEquals(heap.getPriority("a"), 5); + * ``` + * + * @param key The key to insert or update. + * @param priority The priority to set. + */ + pushOrUpdate(key: K, priority: number): void { + assertValidPriority(priority); + const pos = this.#index.get(key); + if (pos !== undefined) { + this.#data[pos]!.priority = priority; + const afterUp = this.#siftUp(pos); + if (afterUp === pos) { + this.#siftDown(pos); + } + } else { + const newPos = this.#data.length; + this.#data.push({ key, priority }); + this.#siftUp(newPos); + } + } + + /** + * Check whether the key is in the heap. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 1); + * + * assertEquals(heap.has("a"), true); + * assertEquals(heap.has("b"), false); + * ``` + * + * @param key The key to look up. + * @returns `true` if the key is present, `false` otherwise. + */ + has(key: K): boolean { + return this.#index.has(key); + } + + /** + * Return the priority of the given key, or `undefined` if not present. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 42); + * + * assertEquals(heap.getPriority("a"), 42); + * assertEquals(heap.getPriority("b"), undefined); + * ``` + * + * @param key The key to look up. + * @returns The priority of the key, or `undefined` if not present. + */ + getPriority(key: K): number | undefined { + const pos = this.#index.get(key); + if (pos === undefined) return undefined; + return this.#data[pos]!.priority; + } + + /** + * The number of entries in the heap. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * assertEquals(heap.size, 0); + * heap.push("a", 1); + * assertEquals(heap.size, 1); + * ``` + * + * @returns The number of entries in the heap. + */ + get size(): number { + return this.#data.length; + } + + /** + * Remove all entries from the heap. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 1); + * heap.push("b", 2); + * heap.clear(); + * + * assertEquals(heap.size, 0); + * assertEquals(heap.isEmpty(), true); + * ``` + */ + clear(): void { + this.#data.length = 0; + this.#index.clear(); + } + + /** + * Check whether the heap is empty. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * assertEquals(heap.isEmpty(), true); + * + * heap.push("a", 1); + * assertEquals(heap.isEmpty(), false); + * ``` + * + * @returns `true` if the heap is empty, `false` otherwise. + */ + isEmpty(): boolean { + return this.#data.length === 0; + } + + /** + * Create an iterator that removes and yields every entry from + * smallest to largest priority. The heap is empty when the iterator + * finishes. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 3); + * heap.push("b", 1); + * heap.push("c", 2); + * + * assertEquals([...heap.drain()], [ + * { key: "b", priority: 1 }, + * { key: "c", priority: 2 }, + * { key: "a", priority: 3 }, + * ]); + * assertEquals(heap.size, 0); + * ``` + * + * @returns An iterator yielding entries from smallest to largest priority. + */ + *drain(): IterableIterator> { + while (!this.isEmpty()) { + yield this.pop() as HeapEntry; + } + } + + /** + * Create an iterator that removes and yields every entry from + * smallest to largest priority. The heap is empty afterwards. + * + * This has the same behavior as + * {@linkcode IndexedHeap.prototype.drain | drain}. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Usage + * ```ts + * import { IndexedHeap } from "@std/data-structures/unstable-indexed-heap"; + * import { assertEquals } from "@std/assert"; + * + * const heap = new IndexedHeap(); + * heap.push("a", 3); + * heap.push("b", 1); + * heap.push("c", 2); + * + * assertEquals([...heap], [ + * { key: "b", priority: 1 }, + * { key: "c", priority: 2 }, + * { key: "a", priority: 3 }, + * ]); + * assertEquals([...heap], []); + * ``` + * + * @returns An iterator yielding entries from smallest to largest priority. + */ + *[Symbol.iterator](): IterableIterator> { + yield* this.drain(); + } +} diff --git a/data_structures/unstable_indexed_heap_test.ts b/data_structures/unstable_indexed_heap_test.ts new file mode 100644 index 000000000000..554750b1e783 --- /dev/null +++ b/data_structures/unstable_indexed_heap_test.ts @@ -0,0 +1,551 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +import { assertEquals, assertThrows } from "@std/assert"; +import { + type HeapEntry, + IndexedHeap, + type ReadonlyIndexedHeap, +} from "./unstable_indexed_heap.ts"; + +Deno.test("IndexedHeap push / pop / peek with ascending priorities", () => { + const heap = new IndexedHeap(); + + assertEquals(heap.size, 0); + assertEquals(heap.isEmpty(), true); + assertEquals(heap.peek(), undefined); + assertEquals(heap.pop(), undefined); + + for ( + const [key, priority] of [["d", 4], ["b", 2], ["e", 5], ["a", 1], [ + "c", + 3, + ]] as const + ) { + heap.push(key, priority); + } + + assertEquals(heap.size, 5); + assertEquals(heap.isEmpty(), false); + assertEquals(heap.peek(), { key: "a", priority: 1 }); + + const popped: HeapEntry[] = []; + while (!heap.isEmpty()) { + popped.push(heap.pop()!); + } + assertEquals(popped, [ + { key: "a", priority: 1 }, + { key: "b", priority: 2 }, + { key: "c", priority: 3 }, + { key: "d", priority: 4 }, + { key: "e", priority: 5 }, + ]); + assertEquals(heap.size, 0); + assertEquals(heap.peek(), undefined); + assertEquals(heap.pop(), undefined); +}); + +Deno.test("IndexedHeap push throws on duplicate key", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + assertThrows( + () => heap.push("a", 2), + Error, + "Cannot push into IndexedHeap: key already exists", + ); +}); + +Deno.test("IndexedHeap delete root", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 2); + heap.push("c", 3); + + assertEquals(heap.delete("a"), true); + assertEquals(heap.size, 2); + assertEquals(heap.has("a"), false); + assertEquals(heap.peek(), { key: "b", priority: 2 }); + + assertEquals([...heap], [ + { key: "b", priority: 2 }, + { key: "c", priority: 3 }, + ]); +}); + +Deno.test("IndexedHeap delete middle element triggers sift-down", () => { + // Heap shape (array order): a=1, b=5, c=3, d=10, e=8 + // a(1) + // / \ + // b(5) c(3) + // / \ + // d(10) e(8) + // Deleting "c" (index 2) moves "e" (priority 8) into index 2. + // 8 > children? No children at that index, so it stays — but "c" is gone. + // Deleting "b" (index 1) moves last element into index 1 and sifts down. + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 5); + heap.push("c", 3); + heap.push("d", 10); + heap.push("e", 8); + + heap.delete("b"); + assertEquals(heap.size, 4); + assertEquals(heap.has("b"), false); + + const result = [...heap]; + assertEquals(result, [ + { key: "a", priority: 1 }, + { key: "c", priority: 3 }, + { key: "e", priority: 8 }, + { key: "d", priority: 10 }, + ]); +}); + +Deno.test("IndexedHeap delete last array element (no sift needed)", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 2); + + assertEquals(heap.delete("b"), true); + assertEquals(heap.size, 1); + assertEquals(heap.peek(), { key: "a", priority: 1 }); +}); + +Deno.test("IndexedHeap delete and getPriority for non-existent key", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + assertEquals(heap.delete("z"), false); + assertEquals(heap.getPriority("z"), undefined); + assertEquals(heap.size, 1); +}); + +Deno.test("IndexedHeap delete only element", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + assertEquals(heap.delete("a"), true); + assertEquals(heap.size, 0); + assertEquals(heap.isEmpty(), true); + assertEquals(heap.peek(), undefined); +}); + +Deno.test("IndexedHeap delete triggers sift-up when replacement is smaller", () => { + // Heap array: [r(1), a(50), b(3), c(51), d(52), e(4), f(5)] + // r(1) + // / \ + // a(50) b(3) + // / \ / \ + // c(51) d(52) e(4) f(5) + // + // Deleting "c" (index 3) moves last element "f" (priority 5) into index 3. + // Parent of index 3 is "a" (priority 50). Since 5 < 50, "f" sifts up. + const heap = new IndexedHeap(); + for ( + const [key, priority] of [ + ["r", 1], + ["a", 50], + ["b", 3], + ["c", 51], + ["d", 52], + ["e", 4], + ["f", 5], + ] as const + ) { + heap.push(key, priority); + } + + heap.delete("c"); + + assertEquals(heap.peek(), { key: "r", priority: 1 }); + assertEquals(heap.has("c"), false); + assertEquals(heap.size, 6); + assertEquals(heap.getPriority("f"), 5); + + assertEquals([...heap], [ + { key: "r", priority: 1 }, + { key: "b", priority: 3 }, + { key: "e", priority: 4 }, + { key: "f", priority: 5 }, + { key: "a", priority: 50 }, + { key: "d", priority: 52 }, + ]); +}); + +Deno.test("IndexedHeap update decrease-key bubbles up", () => { + const heap = new IndexedHeap(); + heap.push("a", 10); + heap.push("b", 20); + heap.push("c", 30); + + heap.update("c", 1); + assertEquals(heap.peek(), { key: "c", priority: 1 }); + assertEquals(heap.getPriority("c"), 1); +}); + +Deno.test("IndexedHeap update increase-key bubbles down", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 2); + heap.push("c", 3); + + heap.update("a", 100); + assertEquals(heap.peek(), { key: "b", priority: 2 }); + + assertEquals([...heap], [ + { key: "b", priority: 2 }, + { key: "c", priority: 3 }, + { key: "a", priority: 100 }, + ]); +}); + +Deno.test("IndexedHeap update throws for non-existent key", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + assertThrows( + () => heap.update("z", 5), + Error, + "Cannot update IndexedHeap: key does not exist", + ); +}); + +Deno.test("IndexedHeap pushOrUpdate inserts when absent, updates when present", () => { + const heap = new IndexedHeap(); + heap.pushOrUpdate("a", 10); + assertEquals(heap.size, 1); + assertEquals(heap.getPriority("a"), 10); + + heap.pushOrUpdate("a", 5); + assertEquals(heap.size, 1); + assertEquals(heap.getPriority("a"), 5); + assertEquals(heap.peek(), { key: "a", priority: 5 }); +}); + +Deno.test("IndexedHeap pushOrUpdate decrease then increase same key", () => { + const heap = new IndexedHeap(); + heap.push("a", 10); + heap.push("b", 20); + heap.push("c", 30); + + heap.pushOrUpdate("c", 1); + assertEquals(heap.peek(), { key: "c", priority: 1 }); + + heap.pushOrUpdate("c", 50); + assertEquals(heap.peek(), { key: "a", priority: 10 }); + + assertEquals([...heap], [ + { key: "a", priority: 10 }, + { key: "b", priority: 20 }, + { key: "c", priority: 50 }, + ]); +}); + +Deno.test("IndexedHeap size tracks push, pop, delete, clear", () => { + const heap = new IndexedHeap(); + assertEquals(heap.size, 0); + + heap.push("a", 1); + assertEquals(heap.size, 1); + + heap.push("b", 2); + assertEquals(heap.size, 2); + + heap.pop(); + assertEquals(heap.size, 1); + + heap.push("c", 3); + heap.delete("b"); + assertEquals(heap.size, 1); + + heap.clear(); + assertEquals(heap.size, 0); +}); + +Deno.test("IndexedHeap iterator drains in ascending order", () => { + const heap = new IndexedHeap(); + heap.push("c", 3); + heap.push("a", 1); + heap.push("b", 2); + + assertEquals([...heap], [ + { key: "a", priority: 1 }, + { key: "b", priority: 2 }, + { key: "c", priority: 3 }, + ]); + assertEquals(heap.size, 0); + assertEquals([...heap], []); +}); + +Deno.test("IndexedHeap drain() yields in ascending order", () => { + const heap = new IndexedHeap(); + heap.push("x", 10); + heap.push("y", 5); + heap.push("z", 15); + + assertEquals([...heap.drain()], [ + { key: "y", priority: 5 }, + { key: "x", priority: 10 }, + { key: "z", priority: 15 }, + ]); + assertEquals(heap.size, 0); +}); + +Deno.test("IndexedHeap drain() on empty heap yields nothing", () => { + const heap = new IndexedHeap(); + assertEquals([...heap.drain()], []); +}); + +Deno.test("IndexedHeap peek returns a copy, not a reference", () => { + const heap = new IndexedHeap(); + heap.push("a", 10); + + const peeked = heap.peek()!; + (peeked as { priority: number }).priority = 999; + + assertEquals(heap.peek()!.priority, 10); +}); + +Deno.test("IndexedHeap is assignable to ReadonlyIndexedHeap", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 2); + + const ro: ReadonlyIndexedHeap = heap; + assertEquals(ro.peek(), { key: "a", priority: 1 }); + assertEquals(ro.has("a"), true); + assertEquals(ro.has("z"), false); + assertEquals(ro.getPriority("a"), 1); + assertEquals(ro.getPriority("z"), undefined); + assertEquals(ro.size, 2); + assertEquals(ro.isEmpty(), false); + + assertEquals(heap.size, 2, "heap unchanged after readonly queries"); +}); + +Deno.test("IndexedHeap handles duplicate priorities", () => { + const heap = new IndexedHeap(); + heap.push("a", 5); + heap.push("b", 5); + heap.push("c", 5); + + assertEquals(heap.size, 3); + const results: HeapEntry[] = []; + while (!heap.isEmpty()) { + results.push(heap.pop()!); + } + assertEquals(results.length, 3); + for (const entry of results) { + assertEquals(entry.priority, 5); + } + assertEquals(results.map((e) => e.key).sort(), ["a", "b", "c"]); +}); + +Deno.test("IndexedHeap with object keys uses reference identity", () => { + const keyA = { id: "a" }; + const keyB = { id: "b" }; + const keyADuplicate = { id: "a" }; + + const heap = new IndexedHeap<{ id: string }>(); + heap.push(keyA, 1); + heap.push(keyB, 2); + heap.push(keyADuplicate, 3); + + assertEquals(heap.size, 3); + assertEquals(heap.has(keyA), true); + assertEquals(heap.has(keyADuplicate), true); + + assertEquals(heap.getPriority(keyA), 1); + assertEquals(heap.getPriority(keyADuplicate), 3); +}); + +Deno.test("IndexedHeap handles negative priorities", () => { + const heap = new IndexedHeap(); + heap.push("a", -10); + heap.push("b", -5); + heap.push("c", 0); + heap.push("d", 5); + + assertEquals([...heap], [ + { key: "a", priority: -10 }, + { key: "b", priority: -5 }, + { key: "c", priority: 0 }, + { key: "d", priority: 5 }, + ]); +}); + +Deno.test("IndexedHeap handles Infinity and -Infinity priorities", () => { + const heap = new IndexedHeap(); + heap.push("pos", Infinity); + heap.push("neg", -Infinity); + heap.push("zero", 0); + + assertEquals([...heap], [ + { key: "neg", priority: -Infinity }, + { key: "zero", priority: 0 }, + { key: "pos", priority: Infinity }, + ]); +}); + +Deno.test("IndexedHeap works correctly after clear and reuse", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + heap.push("b", 2); + heap.clear(); + + heap.push("c", 30); + heap.push("d", 10); + heap.push("e", 20); + + assertEquals(heap.size, 3); + assertEquals(heap.peek(), { key: "d", priority: 10 }); + assertEquals(heap.has("a"), false); + assertEquals(heap.has("d"), true); + + assertEquals([...heap], [ + { key: "d", priority: 10 }, + { key: "e", priority: 20 }, + { key: "c", priority: 30 }, + ]); +}); + +Deno.test("IndexedHeap interleaved push, pop, update, delete", () => { + const heap = new IndexedHeap(); + heap.push("a", 10); + heap.push("b", 20); + heap.push("c", 30); + + assertEquals(heap.pop(), { key: "a", priority: 10 }); + + heap.push("d", 5); + heap.update("c", 1); + + assertEquals(heap.peek(), { key: "c", priority: 1 }); + + heap.delete("b"); + heap.push("e", 3); + + assertEquals(heap.size, 3); + assertEquals([...heap], [ + { key: "c", priority: 1 }, + { key: "e", priority: 3 }, + { key: "d", priority: 5 }, + ]); +}); + +Deno.test("IndexedHeap pop with two elements exercises general path", () => { + const heap = new IndexedHeap(); + heap.push("big", 100); + heap.push("small", 1); + + // pop() with size=2 takes the general path: move last to root, sift-down + assertEquals(heap.pop(), { key: "small", priority: 1 }); + assertEquals(heap.size, 1); + assertEquals(heap.pop(), { key: "big", priority: 100 }); +}); + +Deno.test("IndexedHeap push throws on NaN priority", () => { + const heap = new IndexedHeap(); + assertThrows( + () => heap.push("a", NaN), + RangeError, + "Cannot set priority: value is NaN", + ); + assertEquals(heap.size, 0); +}); + +Deno.test("IndexedHeap update throws on NaN priority", () => { + const heap = new IndexedHeap(); + heap.push("a", 1); + assertThrows( + () => heap.update("a", NaN), + RangeError, + "Cannot set priority: value is NaN", + ); + assertEquals(heap.getPriority("a"), 1); +}); + +Deno.test("IndexedHeap pushOrUpdate throws on NaN priority", () => { + const heap = new IndexedHeap(); + assertThrows( + () => heap.pushOrUpdate("a", NaN), + RangeError, + "Cannot set priority: value is NaN", + ); + assertEquals(heap.size, 0); + + heap.push("b", 1); + assertThrows( + () => heap.pushOrUpdate("b", NaN), + RangeError, + "Cannot set priority: value is NaN", + ); + assertEquals(heap.getPriority("b"), 1); +}); + +Deno.test("IndexedHeap has correct Symbol.toStringTag", () => { + const heap = new IndexedHeap(); + assertEquals(heap[Symbol.toStringTag], "IndexedHeap"); + assertEquals(Object.prototype.toString.call(heap), "[object IndexedHeap]"); +}); + +Deno.test("IndexedHeap stress test: push N, pop all, verify sorted", () => { + const heap = new IndexedHeap(); + const n = 200; + const priorities: number[] = []; + for (let i = 0; i < n; i++) { + const p = Math.floor(Math.random() * 10000); + priorities.push(p); + heap.push(i, p); + } + + assertEquals(heap.size, n); + + const popped: number[] = []; + while (!heap.isEmpty()) { + popped.push(heap.pop()!.priority); + } + + for (let i = 1; i < popped.length; i++) { + if (popped[i]! < popped[i - 1]!) { + throw new Error( + `Heap invariant violated: ${popped[i - 1]} > ${ + popped[i] + } at index ${i}`, + ); + } + } + assertEquals(popped.length, n); +}); + +Deno.test("IndexedHeap stress test: push N, delete random subset, pop rest", () => { + const heap = new IndexedHeap(); + const n = 200; + for (let i = 0; i < n; i++) { + heap.push(i, Math.floor(Math.random() * 10000)); + } + + const toDelete = new Set(); + for (let i = 0; i < n / 2; i++) { + const key = Math.floor(Math.random() * n); + if (!toDelete.has(key)) { + toDelete.add(key); + heap.delete(key); + } + } + + const remaining = n - toDelete.size; + assertEquals(heap.size, remaining); + + const popped: number[] = []; + while (!heap.isEmpty()) { + popped.push(heap.pop()!.priority); + } + + for (let i = 1; i < popped.length; i++) { + if (popped[i]! < popped[i - 1]!) { + throw new Error( + `Heap invariant violated after deletes: ${popped[i - 1]} > ${ + popped[i] + } at index ${i}`, + ); + } + } + assertEquals(popped.length, remaining); +});