UNPKG

max-heap-typed

Version:
693 lines (692 loc) 21.5 kB
"use strict"; /** * data-structure-typed * * @author Pablo Zeng * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com> * @license MIT License */ Object.defineProperty(exports, "__esModule", { value: true }); exports.LinkedHashMap = exports.HashMap = void 0; const base_1 = require("../base"); const utils_1 = require("../../utils"); /** * Hash-based map. Supports object keys and custom hashing; offers O(1) average set/get/has. * @remarks Time O(1), Space O(1) * @template K * @template V * @template R * 1. Key-Value Pair Storage: HashMap stores key-value pairs. Each key map to a value. * 2. Fast Lookup: It's used when you need to quickly find, insert, or delete entries based on a key. * 3. Unique Keys: Keys are unique. * If you try to insert another entry with the same key, the new one will replace the old entry. * 4. Unordered Collection: HashMap does not guarantee the order of entries, and the order may change over time. * @example * // should maintain insertion order * const linkedHashMap = new LinkedHashMap<number, string>(); * linkedHashMap.set(1, 'A'); * linkedHashMap.set(2, 'B'); * linkedHashMap.set(3, 'C'); * * const result = Array.from(linkedHashMap); * console.log(result); // [ * // [1, 'A'], * // [2, 'B'], * // [3, 'C'] * // ] * @example * // fast lookup of values by key * const hashMap = new HashMap<number, string>(); * hashMap.set(1, 'A'); * hashMap.set(2, 'B'); * hashMap.set(3, 'C'); * * console.log(hashMap.get(1)); // 'A' * console.log(hashMap.get(2)); // 'B' * console.log(hashMap.get(3)); // 'C' * console.log(hashMap.get(99)); // undefined * @example * // remove duplicates when adding multiple entries * const hashMap = new HashMap<number, string>(); * hashMap.set(1, 'A'); * hashMap.set(2, 'B'); * hashMap.set(1, 'C'); // Update value for key 1 * * console.log(hashMap.size); // 2 * console.log(hashMap.get(1)); // 'C' * console.log(hashMap.get(2)); // 'B' * @example * // count occurrences of keys * const data = [1, 2, 1, 3, 2, 1]; * * const countMap = new HashMap<number, number>(); * for (const key of data) { * countMap.set(key, (countMap.get(key) || 0) + 1); * } * * console.log(countMap.get(1)); // 3 * console.log(countMap.get(2)); // 2 * console.log(countMap.get(3)); // 1 */ class HashMap extends base_1.IterableEntryBase { /** * Create a HashMap and optionally bulk-insert entries. * @remarks Time O(N), Space O(N) * @param [entryOrRawElements] - Iterable of entries or raw elements to insert. * @param [options] - Options: hash function and optional record-to-entry converter. * @returns New HashMap instance. */ constructor(entryOrRawElements = [], options) { super(); this._store = {}; this._objMap = new Map(); this._size = 0; this._hashFn = (key) => String(key); if (options) { const { hashFn, toEntryFn } = options; if (hashFn) this._hashFn = hashFn; if (toEntryFn) this._toEntryFn = toEntryFn; } if (entryOrRawElements) this.setMany(entryOrRawElements); } /** * Get the internal store for non-object keys. * @remarks Time O(1), Space O(1) * @returns Internal record of string→{key,value}. */ get store() { return this._store; } /** * Get the internal Map used for object/function keys. * @remarks Time O(1), Space O(1) * @returns Map of object→value. */ get objMap() { return this._objMap; } /** * Get the raw→entry converter function if present. * @remarks Time O(1), Space O(1) * @returns Converter function or undefined. */ get toEntryFn() { return this._toEntryFn; } /** * Get the number of distinct keys stored. * @remarks Time O(1), Space O(1) * @returns Current size. */ get size() { return this._size; } /** * Get the current hash function for non-object keys. * @remarks Time O(1), Space O(1) * @returns Hash function. */ get hashFn() { return this._hashFn; } /** * Check whether the map is empty. * @remarks Time O(1), Space O(1) * @returns True if size is 0. */ isEmpty() { return this._size === 0; } /** * Remove all entries and reset counters. * @remarks Time O(N), Space O(1) * @returns void */ clear() { this._store = {}; this._objMap.clear(); this._size = 0; } /** * Type guard: check if a raw value is a [key, value] entry. * @remarks Time O(1), Space O(1) * @returns True if the value is a 2-tuple. */ isEntry(rawElement) { return Array.isArray(rawElement) && rawElement.length === 2; } /** * Insert or replace a single entry. * @remarks Time O(1), Space O(1) * @param key - Key. * @param value - Value. * @returns True when the operation succeeds. */ set(key, value) { if (this._isObjKey(key)) { if (!this.objMap.has(key)) this._size++; this.objMap.set(key, value); } else { const strKey = this._getNoObjKey(key); if (this.store[strKey] === undefined) this._size++; this._store[strKey] = { key, value }; } return true; } /** * Insert many entries from an iterable. * @remarks Time O(N), Space O(N) * @param entryOrRawElements - Iterable of entries or raw elements to insert. * @returns Array of per-entry results. */ setMany(entryOrRawElements) { const results = []; for (const rawEle of entryOrRawElements) { let key, value; if (this.isEntry(rawEle)) [key, value] = rawEle; else if (this._toEntryFn) [key, value] = this._toEntryFn(rawEle); if (key !== undefined && value !== undefined) results.push(this.set(key, value)); } return results; } /** * Get the value for a key. * @remarks Time O(1), Space O(1) * @param key - Key to look up. * @returns Value or undefined. */ get(key) { var _a; if (this._isObjKey(key)) return this.objMap.get(key); const strKey = this._getNoObjKey(key); return (_a = this._store[strKey]) === null || _a === void 0 ? void 0 : _a.value; } /** * Check if a key exists. * @remarks Time O(1), Space O(1) * @param key - Key to test. * @returns True if present. */ has(key) { if (this._isObjKey(key)) return this.objMap.has(key); const strKey = this._getNoObjKey(key); return strKey in this.store; } /** * Delete an entry by key. * @remarks Time O(1), Space O(1) * @param key - Key to delete. * @returns True if the key was found and removed. */ delete(key) { if (this._isObjKey(key)) { if (this.objMap.has(key)) this._size--; return this.objMap.delete(key); } const strKey = this._getNoObjKey(key); if (strKey in this.store) { delete this.store[strKey]; this._size--; return true; } return false; } /** * Replace the hash function and rehash the non-object store. * @remarks Time O(N), Space O(N) * @param fn - New hash function for non-object keys. * @returns This map instance. */ setHashFn(fn) { if (this._hashFn === fn) return this; this._hashFn = fn; this._rehashNoObj(); return this; } /** * Deep clone this map, preserving hashing behavior. * @remarks Time O(N), Space O(N) * @returns A new map with the same content. */ clone() { const opts = { hashFn: this._hashFn, toEntryFn: this._toEntryFn }; return this._createLike(this, opts); } /** * Map values to a new map with the same keys. * @remarks Time O(N), Space O(N) * @template VM * @param callbackfn - Mapping function (key, value, index, map) → newValue. * @param [thisArg] - Value for `this` inside the callback. * @returns A new map with transformed values. */ map(callbackfn, thisArg) { const out = this._createLike(); let index = 0; for (const [key, value] of this) out.set(key, callbackfn.call(thisArg, key, value, index++, this)); return out; } /** * Filter entries into a new map. * @remarks Time O(N), Space O(N) * @param predicate - Predicate (key, value, index, map) → boolean. * @param [thisArg] - Value for `this` inside the predicate. * @returns A new map containing entries that satisfied the predicate. */ filter(predicate, thisArg) { const out = this._createLike(); let index = 0; for (const [key, value] of this) if (predicate.call(thisArg, key, value, index++, this)) out.set(key, value); return out; } /** * (Protected) Create a like-kind instance and seed it from an iterable. * @remarks Time O(N), Space O(N) * @template TK * @template TV * @template TR * @param [entries] - Iterable used to seed the new map. * @param [options] - Options forwarded to the constructor. * @returns A like-kind map instance. */ _createLike(entries = [], options) { const Ctor = this.constructor; return new Ctor(entries, options); } _rehashNoObj() { const fresh = {}; for (const { key, value } of Object.values(this._store)) { const sk = this._getNoObjKey(key); fresh[sk] = { key, value }; } this._store = fresh; } *_getIterator() { for (const node of Object.values(this.store)) yield [node.key, node.value]; for (const node of this.objMap) yield node; } _isObjKey(key) { const keyType = typeof key; return (keyType === 'object' || keyType === 'function') && key !== null; } _getNoObjKey(key) { const keyType = typeof key; let strKey; if (keyType !== 'string' && keyType !== 'number' && keyType !== 'symbol') { strKey = this._hashFn(key); } else { if (keyType === 'number') { strKey = key; } else { strKey = key; } } return strKey; } } exports.HashMap = HashMap; /** * Hash-based map that preserves insertion order via a doubly-linked list. * @remarks Time O(1), Space O(1) * @template K * @template V * @template R * @example examples will be generated by unit test */ class LinkedHashMap extends base_1.IterableEntryBase { /** * Create a LinkedHashMap and optionally bulk-insert entries. * @remarks Time O(N), Space O(N) * @param [entryOrRawElements] - Iterable of entries or raw elements to insert. * @param [options] - Options: hash functions and optional record-to-entry converter. * @returns New LinkedHashMap instance. */ constructor(entryOrRawElements = [], options) { super(); this._hashFn = (key) => String(key); this._objHashFn = (key) => key; this._noObjMap = {}; this._objMap = new WeakMap(); this._toEntryFn = (rawElement) => { if (this.isEntry(rawElement)) { return rawElement; } throw new Error('If `entryOrRawElements` does not adhere to [key,value], provide `options.toEntryFn` to transform raw records.'); }; this._size = 0; this._sentinel = {}; this._sentinel.prev = this._sentinel.next = this._head = this._tail = this._sentinel; if (options) { const { hashFn, objHashFn, toEntryFn } = options; if (hashFn) this._hashFn = hashFn; if (objHashFn) this._objHashFn = objHashFn; if (toEntryFn) this._toEntryFn = toEntryFn; } if (entryOrRawElements) this.setMany(entryOrRawElements); } get hashFn() { return this._hashFn; } /** * Get the hash function for object/weak keys. * @remarks Time O(1), Space O(1) * @returns Object-hash function. */ get objHashFn() { return this._objHashFn; } /** * Get the internal record for non-object keys. * @remarks Time O(1), Space O(1) * @returns Record of hash→node. */ get noObjMap() { return this._noObjMap; } get objMap() { return this._objMap; } /** * Get the head node (first entry) sentinel link. * @remarks Time O(1), Space O(1) * @returns Head node or sentinel. */ get head() { return this._head; } /** * Get the tail node (last entry) sentinel link. * @remarks Time O(1), Space O(1) * @returns Tail node or sentinel. */ get tail() { return this._tail; } get toEntryFn() { return this._toEntryFn; } get size() { return this._size; } /** * Get the first [key, value] pair. * @remarks Time O(1), Space O(1) * @returns First entry or undefined when empty. */ get first() { if (this._size === 0) return; return [this.head.key, this.head.value]; } /** * Get the last [key, value] pair. * @remarks Time O(1), Space O(1) * @returns Last entry or undefined when empty. */ get last() { if (this._size === 0) return; return [this.tail.key, this.tail.value]; } /** * Iterate from head → tail. * @remarks Time O(N), Space O(1) * @returns Iterator of [key, value]. */ *begin() { let node = this.head; while (node !== this._sentinel) { yield [node.key, node.value]; node = node.next; } } /** * Iterate from tail → head. * @remarks Time O(N), Space O(1) * @returns Iterator of [key, value]. */ *reverseBegin() { let node = this.tail; while (node !== this._sentinel) { yield [node.key, node.value]; node = node.prev; } } /** * Insert or replace a single entry; preserves insertion order. * @remarks Time O(1), Space O(1) * @param key - Key. * @param [value] - Value. * @returns True when the operation succeeds. */ set(key, value) { let node; const isNewKey = !this.has(key); if ((0, utils_1.isWeakKey)(key)) { const hash = this._objHashFn(key); node = this.objMap.get(hash); if (!node && isNewKey) { node = { key: hash, value, prev: this.tail, next: this._sentinel }; this.objMap.set(hash, node); } else if (node) { node.value = value; } } else { const hash = this._hashFn(key); node = this.noObjMap[hash]; if (!node && isNewKey) { this.noObjMap[hash] = node = { key, value, prev: this.tail, next: this._sentinel }; } else if (node) { node.value = value; } } if (node && isNewKey) { if (this._size === 0) { this._head = node; this._sentinel.next = node; } else { this.tail.next = node; node.prev = this.tail; } this._tail = node; this._sentinel.prev = node; this._size++; } return true; } setMany(entryOrRawElements) { const results = []; for (const rawEle of entryOrRawElements) { let key, value; if (this.isEntry(rawEle)) [key, value] = rawEle; else if (this._toEntryFn) [key, value] = this._toEntryFn(rawEle); if (key !== undefined && value !== undefined) results.push(this.set(key, value)); } return results; } has(key) { if ((0, utils_1.isWeakKey)(key)) { const hash = this._objHashFn(key); return this.objMap.has(hash); } const hash = this._hashFn(key); return hash in this.noObjMap; } get(key) { if ((0, utils_1.isWeakKey)(key)) { const hash = this._objHashFn(key); const node = this.objMap.get(hash); return node ? node.value : undefined; } const hash = this._hashFn(key); const node = this.noObjMap[hash]; return node ? node.value : undefined; } /** * Get the value at a given index in insertion order. * @remarks Time O(N), Space O(1) * @param index - Zero-based index. * @returns Value at the index. */ at(index) { (0, utils_1.rangeCheck)(index, 0, this._size - 1); let node = this.head; while (index--) node = node.next; return node.value; } delete(key) { let node; if ((0, utils_1.isWeakKey)(key)) { const hash = this._objHashFn(key); node = this.objMap.get(hash); if (!node) return false; this.objMap.delete(hash); } else { const hash = this._hashFn(key); node = this.noObjMap[hash]; if (!node) return false; delete this.noObjMap[hash]; } return this._deleteNode(node); } /** * Delete the first entry that matches a predicate. * @remarks Time O(N), Space O(1) * @param predicate - Function (key, value, index, map) → boolean to decide deletion. * @returns True if an entry was removed. */ deleteWhere(predicate) { let node = this._head; let i = 0; while (node !== this._sentinel) { const cur = node; node = node.next; if (predicate(cur.key, cur.value, i++, this)) { if ((0, utils_1.isWeakKey)(cur.key)) { this._objMap.delete(cur.key); } else { const hash = this._hashFn(cur.key); delete this._noObjMap[hash]; } return this._deleteNode(cur); } } return false; } /** * Delete the entry at a given index. * @remarks Time O(N), Space O(1) * @param index - Zero-based index. * @returns True if removed. */ deleteAt(index) { (0, utils_1.rangeCheck)(index, 0, this._size - 1); let node = this.head; while (index--) node = node.next; return this._deleteNode(node); } isEmpty() { return this._size === 0; } isEntry(rawElement) { return Array.isArray(rawElement) && rawElement.length === 2; } clear() { this._noObjMap = {}; this._size = 0; this._head = this._tail = this._sentinel.prev = this._sentinel.next = this._sentinel; } clone() { const opts = { hashFn: this._hashFn, objHashFn: this._objHashFn }; return this._createLike(this, opts); } filter(predicate, thisArg) { const out = this._createLike(); let index = 0; for (const [key, value] of this) { if (predicate.call(thisArg, key, value, index, this)) out.set(key, value); index++; } return out; } /** * Map each entry to a new [key, value] pair and preserve order. * @remarks Time O(N), Space O(N) * @template MK * @template MV * @param callback - Mapping function (key, value, index, map) → [newKey, newValue]. * @param [thisArg] - Value for `this` inside the callback. * @returns A new map of the same class with transformed entries. */ map(callback, thisArg) { const out = this._createLike(); let index = 0; for (const [key, value] of this) { const [newKey, newValue] = callback.call(thisArg, key, value, index, this); out.set(newKey, newValue); index++; } return out; } *_getIterator() { let node = this.head; while (node !== this._sentinel) { yield [node.key, node.value]; node = node.next; } } _deleteNode(node) { const { prev, next } = node; prev.next = next; next.prev = prev; if (node === this.head) this._head = next; if (node === this.tail) this._tail = prev; this._size -= 1; return true; } _createLike(entries = [], options) { const Ctor = this.constructor; return new Ctor(entries, options); } } exports.LinkedHashMap = LinkedHashMap;