max-heap-typed
Version:
693 lines (692 loc) • 21.5 kB
JavaScript
"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;