UNPKG

deque-typed

Version:
503 lines (448 loc) 13.6 kB
/** * TreeMultiSet (ordered multiset) — a restricted, native-like API backed by RedBlackTree. * * Design goals: * - No node exposure * - Strict default comparator (number/string/Date), otherwise require comparator * - Default iteration is expanded (each element yielded `count(x)` times) * - `delete(x)` removes one occurrence by default */ import type { Comparator, TreeMultiSetOptions } from '../../types'; import { RedBlackTree } from './red-black-tree'; import { TreeSet } from './tree-set'; export class TreeMultiSet<K = any, R = K> implements Iterable<K> { readonly #core: RedBlackTree<K, number>; readonly #isDefaultComparator: boolean; private _size = 0; // total occurrences (sumCounts) /** * Creates a new TreeMultiSet. * @param elements - Initial elements to add, or raw elements if `toElementFn` is provided. * @param options - Configuration options including optional `toElementFn` to transform raw elements. * @remarks Time O(m log m), Space O(m) where m is the number of initial elements * @example * // Standard usage with elements * const mset = new TreeMultiSet([1, 2, 2, 3]); * * // Using toElementFn to transform raw objects * const items = [{ score: 100 }, { score: 200 }, { score: 100 }]; * const mset = new TreeMultiSet<number, Item>(items, { toElementFn: item => item.score }); */ constructor(elements: Iterable<R> | Iterable<K> = [], options: TreeMultiSetOptions<K, R> = {}) { const toElementFn = options.toElementFn; const comparator = options.comparator ?? TreeSet.createDefaultComparator<K>(); this.#isDefaultComparator = options.comparator === undefined; this.#core = new RedBlackTree<K, number>([], { comparator, isMapMode: options.isMapMode }); for (const item of elements) { const k = toElementFn ? toElementFn(item as R) : (item as K); this.add(k); } } /** * Validates the key against the default comparator rules. * @remarks Time O(1), Space O(1) */ private _validateKey(key: K): void { if (!this.#isDefaultComparator) return; if (typeof key === 'number') { if (Number.isNaN(key)) throw new TypeError('TreeMultiSet: NaN is not a valid key'); return; } if (typeof key === 'string') return; if (key instanceof Date) { if (Number.isNaN(key.getTime())) throw new TypeError('TreeMultiSet: invalid Date key'); return; } throw new TypeError('TreeMultiSet: comparator is required for non-number/non-string/non-Date keys'); } /** * Validates that count is a non-negative safe integer. * @remarks Time O(1), Space O(1) */ private _validateCount(n: number): void { if (!Number.isSafeInteger(n) || n < 0) throw new RangeError('TreeMultiSet: count must be a safe integer >= 0'); } /** * Total occurrences (sumCounts). * @remarks Time O(1), Space O(1) */ get size(): number { return this._size; } /** * Number of distinct keys. * @remarks Time O(1), Space O(1) */ get distinctSize(): number { return this.#core.size; } /** * Whether the multiset is empty. * @remarks Time O(1), Space O(1) */ isEmpty(): boolean { return this.size === 0; } /** * Whether the multiset contains the given key. * @remarks Time O(log n), Space O(1) */ has(key: K): boolean { this._validateKey(key); return this.count(key) > 0; } /** * Returns the count of occurrences for the given key. * @remarks Time O(log n), Space O(1) */ count(key: K): number { this._validateKey(key); return this.#core.get(key) ?? 0; } /** * Add `n` occurrences of `key`. * @returns True if the multiset changed. * @remarks Time O(log n), Space O(1) */ add(key: K, n = 1): boolean { this._validateKey(key); this._validateCount(n); if (n === 0) return false; const old = this.#core.get(key) ?? 0; const next = old + n; this.#core.set(key, next); this._size += n; return true; } /** * Set count for `key` to exactly `n`. * @returns True if changed. * @remarks Time O(log n), Space O(1) */ setCount(key: K, n: number): boolean { this._validateKey(key); this._validateCount(n); const old = this.#core.get(key) ?? 0; if (old === n) return false; if (n === 0) { if (old !== 0) this.#core.delete(key); } else { this.#core.set(key, n); } this._size += n - old; return true; } /** * Delete `n` occurrences of `key` (default 1). * @returns True if any occurrence was removed. * @remarks Time O(log n), Space O(1) */ delete(key: K, n = 1): boolean { this._validateKey(key); this._validateCount(n); if (n === 0) return false; const old = this.#core.get(key) ?? 0; if (old === 0) return false; const removed = Math.min(old, n); const next = old - removed; if (next === 0) this.#core.delete(key); else this.#core.set(key, next); this._size -= removed; return true; } /** * Delete all occurrences of the given key. * @returns True if any occurrence was removed. * @remarks Time O(log n), Space O(1) */ deleteAll(key: K): boolean { this._validateKey(key); const old = this.#core.get(key) ?? 0; if (old === 0) return false; this.#core.delete(key); this._size -= old; return true; } /** * Iterates over distinct keys (each key yielded once). * @remarks Time O(n), Space O(1) */ *keysDistinct(): IterableIterator<K> { yield* this.#core.keys(); } /** * Iterates over entries as [key, count] pairs. * @remarks Time O(n), Space O(1) */ *entries(): IterableIterator<[K, number]> { for (const [k, v] of this.#core) { yield [k, v ?? 0]; } } /** * Expanded iteration (default). Each key is yielded `count(key)` times. * @remarks Time O(size), Space O(1) where size is total occurrences */ *[Symbol.iterator](): Iterator<K> { for (const [k, c] of this.entries()) { for (let i = 0; i < c; i++) yield k; } } /** * Returns an array with all elements (expanded). * @remarks Time O(size), Space O(size) */ toArray(): K[] { return [...this]; } /** * Returns an array with distinct keys only. * @remarks Time O(n), Space O(n) */ toDistinctArray(): K[] { return [...this.keysDistinct()]; } /** * Returns an array of [key, count] entries. * @remarks Time O(n), Space O(n) */ toEntries(): Array<[K, number]> { return [...this.entries()]; } /** * Expose comparator for advanced usage/testing (read-only). * @remarks Time O(1), Space O(1) */ get comparator(): Comparator<K> { return (this.#core as any)._comparator; } // ━━━ clear ━━━ /** * Remove all elements from the multiset. * @remarks Time O(1), Space O(1) * @example * const ms = new TreeMultiSet([1, 2, 2, 3]); * ms.clear(); * ms.size; // 0 */ clear(): void { this.#core.clear(); this._size = 0; } // ━━━ Navigable methods ━━━ /** * Returns the smallest key, or undefined if empty. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([3, 1, 4]); * ms.first(); // 1 */ first(): K | undefined { return this.#core.getLeftMost(); } /** * Returns the largest key, or undefined if empty. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([3, 1, 4]); * ms.last(); // 4 */ last(): K | undefined { return this.#core.getRightMost(); } /** * Removes all occurrences of the smallest key and returns it. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([1, 1, 2, 3]); * ms.pollFirst(); // 1 * ms.has(1); // false */ pollFirst(): K | undefined { const key = this.first(); if (key === undefined) return undefined; this.deleteAll(key); return key; } /** * Removes all occurrences of the largest key and returns it. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([1, 2, 3, 3]); * ms.pollLast(); // 3 * ms.has(3); // false */ pollLast(): K | undefined { const key = this.last(); if (key === undefined) return undefined; this.deleteAll(key); return key; } /** * Returns the smallest key >= given key, or undefined. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([10, 20, 30]); * ms.ceiling(15); // 20 * ms.ceiling(20); // 20 */ ceiling(key: K): K | undefined { this._validateKey(key); return this.#core.ceiling(key); } /** * Returns the largest key <= given key, or undefined. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([10, 20, 30]); * ms.floor(25); // 20 * ms.floor(20); // 20 */ floor(key: K): K | undefined { this._validateKey(key); return this.#core.floor(key); } /** * Returns the smallest key > given key, or undefined. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([10, 20, 30]); * ms.higher(10); // 20 * ms.higher(15); // 20 */ higher(key: K): K | undefined { this._validateKey(key); return this.#core.higher(key); } /** * Returns the largest key < given key, or undefined. * @remarks Time O(log n), Space O(1) * @example * const ms = new TreeMultiSet([10, 20, 30]); * ms.lower(20); // 10 * ms.lower(15); // 10 */ lower(key: K): K | undefined { this._validateKey(key); return this.#core.lower(key); } // ━━━ Functional methods ━━━ /** * Iterates over distinct keys with their counts. * @remarks Time O(n), Space O(1) * @example * const ms = new TreeMultiSet([1, 1, 2, 3, 3, 3]); * ms.forEach((key, count) => console.log(`${key}: ${count}`)); * // 1: 2, 2: 1, 3: 3 */ forEach(callback: (key: K, count: number) => void): void { for (const [k, c] of this.entries()) { callback(k, c); } } /** * Creates a new TreeMultiSet with entries that match the predicate. * @remarks Time O(n log n), Space O(n) * @example * const ms = new TreeMultiSet([1, 1, 2, 3, 3, 3]); * const filtered = ms.filter((key, count) => count >= 2); * // TreeMultiSet { 1: 2, 3: 3 } */ filter(predicate: (key: K, count: number) => boolean): TreeMultiSet<K> { const result = new TreeMultiSet<K>([], { comparator: this.#isDefaultComparator ? undefined : this.comparator, isMapMode: (this.#core as any)._isMapMode }); for (const [k, c] of this.entries()) { if (predicate(k, c)) { result.add(k, c); } } return result; } /** * Reduces the multiset to a single value. * @remarks Time O(n), Space O(1) * @example * const ms = new TreeMultiSet([1, 1, 2, 3, 3, 3]); * const total = ms.reduce((acc, key, count) => acc + count, 0); // 6 */ reduce<U>(callback: (accumulator: U, key: K, count: number) => U, initialValue: U): U { let acc = initialValue; for (const [k, c] of this.entries()) { acc = callback(acc, k, c); } return acc; } /** * Maps keys and counts to a new TreeMultiSet. * When multiple keys map to the same new key, counts are merged (added). * @remarks Time O(n log n), Space O(n) * @example * const ms = new TreeMultiSet([1, 1, 2, 3, 3, 3]); * const mapped = ms.map((key, count) => [key * 10, count]); * // TreeMultiSet { 10: 2, 20: 1, 30: 3 } * @example * // Collision: counts merge * const ms = new TreeMultiSet([1, 2, 3]); * const merged = ms.map((key, count) => [key % 2, count]); * // { 0: 1, 1: 2 } (1 and 3 both map to 1, counts add) */ map<K2>( mapper: (key: K, count: number) => [K2, number], options?: { comparator?: Comparator<K2> } ): TreeMultiSet<K2> { const result = new TreeMultiSet<K2>([], { comparator: options?.comparator, isMapMode: (this.#core as any)._isMapMode }); for (const [k, c] of this.entries()) { const [newKey, newCount] = mapper(k, c); result.add(newKey, newCount); } return result; } /** * Creates an independent copy of this multiset. * @remarks Time O(n log n), Space O(n) * @example * const ms = new TreeMultiSet([1, 1, 2]); * const copy = ms.clone(); * copy.add(3); * ms.has(3); // false (original unchanged) */ clone(): TreeMultiSet<K> { const result = new TreeMultiSet<K>([], { comparator: this.#isDefaultComparator ? undefined : this.comparator, isMapMode: (this.#core as any)._isMapMode }); for (const [k, c] of this.entries()) { result.add(k, c); } return result; } // ━━━ Tree utilities ━━━ /** * Returns keys within the given range. * @remarks Time O(log n + k), Space O(k) where k is result size * @example * const ms = new TreeMultiSet([10, 20, 30, 40, 50]); * ms.rangeSearch([15, 45]); // [20, 30, 40] */ rangeSearch<C extends (key: K) => any>( range: [K, K], callback?: C ): (C extends undefined ? K : ReturnType<C>)[] { const cb = callback ?? ((k: K) => k); return this.#core.rangeSearch(range, node => cb(node.key)) as any; } /** * Prints the internal tree structure (for debugging). * @remarks Time O(n), Space O(n) * @example * const ms = new TreeMultiSet([1, 2, 3]); * ms.print(); */ print(): void { this.#core.print(); } }