From 7c6e5d3b1cdad027bcb97774659c131beacde44b Mon Sep 17 00:00:00 2001 From: Revone Date: Mon, 20 Nov 2023 13:03:39 +0800 Subject: [PATCH] refactor: HashMap supports custom hash functions. --- src/data-structures/hash/hash-map.ts | 226 +++++++++++------- src/utils/utils.ts | 2 +- .../data-structures/hash/hash-map.test.ts | 51 ++-- 3 files changed, 167 insertions(+), 112 deletions(-) diff --git a/src/data-structures/hash/hash-map.ts b/src/data-structures/hash/hash-map.ts index 82bc63d..67bf244 100644 --- a/src/data-structures/hash/hash-map.ts +++ b/src/data-structures/hash/hash-map.ts @@ -6,31 +6,47 @@ * @license MIT License */ -import { isObjOrFunc, rangeCheck } from '../../utils'; -import { HashMapLinkedNode, IterableWithSizeOrLength } from '../../types'; +import { isWeakKey, rangeCheck } from '../../utils'; +import { HashMapLinkedNode } from '../../types'; + +type HashMapOptions = { + elements: Iterable<[K, V]>; + hashFn: (key: K) => string; + objHashFn: (key: K) => WeakKey +} export class HashMap { - readonly OBJ_KEY_INDEX = Symbol('OBJ_KEY_INDEX'); - protected _nodes: HashMapLinkedNode[] = []; - protected _orgMap: Record> = {}; - protected _head: HashMapLinkedNode; - protected _tail: HashMapLinkedNode; - protected readonly _sentinel: HashMapLinkedNode; + + protected _noObjMap: Record> = {}; + protected _objMap = new WeakMap>(); + protected _head: HashMapLinkedNode; + protected _tail: HashMapLinkedNode; + protected readonly _sentinel: HashMapLinkedNode; + protected _hashFn: (key: K) => string; + protected _objHashFn: (key: K) => WeakKey; /** - * The constructor initializes a HashMap object with an optional initial set of key-value pairs. - * @param {Iterable<[K, V]>} elements - The `hashMap` parameter is an optional parameter of type `HashMapOptions<[K, - * V]>`. It is an array of key-value pairs, where each pair is represented as an array `[K, V]`. The - * `K` represents the type of the key and `V` represents the + * The constructor initializes a HashMapLinkedNode with an optional iterable of key-value pairs. + * @param options - The `options` parameter is an object that contains the `elements` property. The + * `elements` property is an iterable that contains key-value pairs represented as arrays `[K, V]`. */ - constructor(elements: IterableWithSizeOrLength<[K, V]> = []) { - Object.setPrototypeOf(this._orgMap, null); + constructor(options: HashMapOptions = { + elements: [], + hashFn: (key: K) => String(key), + objHashFn: (key: K) => (key) + }) { this._sentinel = >{}; this._sentinel.prev = this._sentinel.next = this._head = this._tail = this._sentinel; - for (const el of elements) { - this.set(el[0], el[1]); + const { elements, hashFn, objHashFn } = options; + this._hashFn = hashFn; + this._objHashFn = objHashFn; + if (elements) { + for (const el of elements) { + this.set(el[0], el[1]); + } } + } protected _size = 0; @@ -98,50 +114,54 @@ export class HashMap { * type, but typically it is a string or symbol. * @param {V} [value] - The `value` parameter is an optional parameter of type `V`. It represents the * value associated with the key being set in the data structure. - * @param {boolean} isObjectKey - A boolean flag indicating whether the key is an object key or not. * @returns the size of the data structure after the key-value pair has been set. */ - set(key: K, value?: V, isObjectKey: boolean = isObjOrFunc(key)) { - let newTail; - if (isObjectKey) { - const index = (>(key))[this.OBJ_KEY_INDEX]; - if (index !== undefined) { - this._nodes[index].value = value; - return this._size; - } - Object.defineProperty(key, this.OBJ_KEY_INDEX, { - value: this._nodes.length, - configurable: true - }); - newTail = { - key: key, - value: value, - prev: this._tail, - next: this._sentinel - }; - this._nodes.push(newTail); - } else { - const node = this._orgMap[(key)]; + set(key: K, value?: V) { + let node; + + if (isWeakKey(key)) { + // const hash = this._objHashFn(key); + const hash = key; + node = this._objMap.get(hash); + if (node) { - node.value = value; - return this._size; + // If the node already exists, update its value + node.value = value; + } else { + // Create new node + node = { key: hash, value, prev: this._tail, next: this._sentinel }; + + // Add new nodes to _objMap and linked list + this._objMap.set(hash, node); } - this._orgMap[(key)] = newTail = { - key: key, - value: value, - prev: this._tail, - next: this._sentinel - }; - } - if (this._size === 0) { - this._head = newTail; - this._sentinel.next = newTail; } else { - this._tail.next = newTail; + const hash = this._hashFn(key); + // Non-object keys are handled in the same way as the original implementation + node = this._noObjMap[hash]; + if (node) { + node.value = value; + } else { + this._noObjMap[hash] = node = { + key, + value, + prev: this._tail, + next: this._sentinel + }; + } } - this._tail = newTail; - this._sentinel.prev = newTail; - return ++this._size; + + if (this._size === 0) { + this._head = node; + this._sentinel.next = node; + } else { + this._tail.next = node; + } + + this._tail = node; + this._sentinel.prev = node; + this._size++; + + return this._size; } /** @@ -152,21 +172,21 @@ export class HashMap { * key directly or by using an index stored in the key object. * @param {K} key - The `key` parameter is the key used to retrieve a value from the map. It can be * of any type, but typically it is a string or symbol. - * @param {boolean} isObjectKey - The `isObjectKey` parameter is a boolean flag that indicates - * whether the `key` parameter is an object key or not. If `isObjectKey` is `true`, it means that - * `key` is an object key. If `isObjectKey` is `false`, it means that `key` * @returns The value associated with the given key is being returned. If the key is an object key, * the value is retrieved from the `_nodes` array using the index stored in the `OBJ_KEY_INDEX` - * property of the key. If the key is a string key, the value is retrieved from the `_orgMap` object + * property of the key. If the key is a string key, the value is retrieved from the `_noObjMap` object * using the key itself. If the key is not found, `undefined` is */ - get(key: K, isObjectKey: boolean = isObjOrFunc(key)) { - if (isObjectKey) { - const index = (>(key))[this.OBJ_KEY_INDEX]; - return index !== undefined ? this._nodes[index].value : undefined; + get(key: K): V | undefined { + if (isWeakKey(key)) { + const hash = this._objHashFn(key); + const node = this._objMap.get(hash); + return node ? node.value : undefined; + } else { + const hash = this._hashFn(key); + const node = this._noObjMap[hash]; + return node ? node.value : undefined; } - const node = this._orgMap[(key)]; - return node ? node.value : undefined; } /** @@ -196,25 +216,37 @@ export class HashMap { * The `delete` function removes a key-value pair from a map-like data structure. * @param {K} key - The `key` parameter is the key that you want to delete from the data structure. * It can be of any type, but typically it is a string or an object. - * @param {boolean} isObjectKey - The `isObjectKey` parameter is a boolean flag that indicates - * whether the `key` parameter is an object key or not. If `isObjectKey` is `true`, it means that the - * `key` parameter is an object key. If `isObjectKey` is `false`, it means that the * @returns a boolean value. It returns `true` if the deletion was successful, and `false` if the key * was not found. */ - delete(key: K, isObjectKey: boolean = isObjOrFunc(key)) { + delete(key: K) { let node; - if (isObjectKey) { - const index = (>(key))[this.OBJ_KEY_INDEX]; - if (index === undefined) return false; - delete (>(key))[this.OBJ_KEY_INDEX]; - node = this._nodes[index]; - delete this._nodes[index]; + + if (isWeakKey(key)) { + const hash = this._objHashFn(key); + // Get nodes from WeakMap + node = this._objMap.get(hash); + + if (!node) { + return false; // If the node does not exist, return false + } + + // Remove nodes from WeakMap + this._objMap.delete(hash); } else { - node = this._orgMap[(key)]; - if (node === undefined) return false; - delete this._orgMap[(key)]; + const hash = this._hashFn(key); + // Get nodes from noObjMap + node = this._noObjMap[hash]; + + if (!node) { + return false; // If the node does not exist, return false + } + + // Remove nodes from orgMap + delete this._noObjMap[hash]; } + + // Remove node from doubly linked list this._deleteNode(node); return true; } @@ -257,13 +289,7 @@ export class HashMap { * The `clear` function clears all the elements in a data structure and resets its properties. */ clear() { - // const OBJ_KEY_INDEX = this.OBJ_KEY_INDEX; - // this._nodes.forEach(el => { - // delete (>el.key)[OBJ_KEY_INDEX]; - // }); - this._nodes = []; - this._orgMap = {}; - Object.setPrototypeOf(this._orgMap, null); + this._noObjMap = {}; this._size = 0; this._head = this._tail = this._sentinel.prev = this._sentinel.next = this._sentinel; } @@ -286,6 +312,33 @@ export class HashMap { } } + filter(predicate: (element: [K, V], map: HashMap) => boolean): HashMap { + const filteredMap = new HashMap(); + for (const [key, value] of this) { + if (predicate([key, value], this)) { + filteredMap.set(key, value); + } + } + return filteredMap; + } + + map(callback: (element: [K, V], map: HashMap) => NV): HashMap { + const mappedMap = new HashMap(); + for (const [key, value] of this) { + const newValue = callback([key, value], this); + mappedMap.set(key, newValue); + } + return mappedMap; + } + + reduce(callback: (accumulator: A, element: [K, V], map: HashMap) => A, initialValue: A): A { + let accumulator = initialValue; + for (const element of this) { + accumulator = callback(accumulator, element, this); + } + return accumulator; + } + /** * Time Complexity: O(n), where n is the number of elements in the HashMap. * Space Complexity: O(1) @@ -310,16 +363,19 @@ export class HashMap { * represents a node in a linked list. It contains a key-value pair and references to the previous * and next nodes in the list. */ - protected _deleteNode(node: HashMapLinkedNode) { + protected _deleteNode(node: HashMapLinkedNode) { 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; } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 28f9d86..0b676b5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -93,7 +93,7 @@ export const throwRangeError = (message = 'The value is off-limits.'): void => { throw new RangeError(message); }; -export const isObjOrFunc = (input: unknown): input is Record | ((...args: any[]) => any) => { +export const isWeakKey = (input: unknown): input is WeakKey => { const inputType = typeof input; return (inputType === 'object' && input !== null) || inputType === 'function'; }; diff --git a/test/performance/data-structures/hash/hash-map.test.ts b/test/performance/data-structures/hash/hash-map.test.ts index 38ef805..6536a41 100644 --- a/test/performance/data-structures/hash/hash-map.test.ts +++ b/test/performance/data-structures/hash/hash-map.test.ts @@ -14,6 +14,7 @@ suite.add(`${MILLION.toLocaleString()} set`, () => { hm.set(i, i); } }); + if (isCompetitor) { suite.add(`${MILLION.toLocaleString()} CPT set`, () => { const hm = new CHashMap(); @@ -24,28 +25,6 @@ if (isCompetitor) { }); } -suite.add(`${MILLION.toLocaleString()} set & get`, () => { - const hm = new HashMap(); - - for (let i = 0; i < MILLION; i++) { - hm.set(i, i); - } - for (let i = 0; i < MILLION; i++) { - hm.get(i); - } -}); - -suite.add(`${MILLION.toLocaleString()} Map set & get`, () => { - const hm = new Map(); - - for (let i = 0; i < MILLION; i++) { - hm.set(i, i); - } - for (let i = 0; i < MILLION; i++) { - hm.get(i); - } -}); - suite.add(`${MILLION.toLocaleString()} Map set`, () => { const hm = new Map(); @@ -58,14 +37,14 @@ suite.add(`${MILLION.toLocaleString()} Set add`, () => { for (let i = 0; i < MILLION; i++) hs.add(i); }); -suite.add(`${MILLION.toLocaleString()} Set add & has`, () => { - const hs = new Set(); +suite.add(`${MILLION.toLocaleString()} set & get`, () => { + const hm = new HashMap(); for (let i = 0; i < MILLION; i++) { - hs.add(i); + hm.set(i, i); } for (let i = 0; i < MILLION; i++) { - hs.has(i); + hm.get(i); } }); @@ -82,6 +61,26 @@ if (isCompetitor) { }); } +suite.add(`${MILLION.toLocaleString()} Map set & get`, () => { + const hm = new Map(); + + for (let i = 0; i < MILLION; i++) { + hm.set(i, i); + } + for (let i = 0; i < MILLION; i++) { + hm.get(i); + } +}); + +suite.add(`${MILLION.toLocaleString()} Set add & has`, () => { + const hs = new Set(); + + for (let i = 0; i < MILLION; i++) hs.add(i); + + for (let i = 0; i < MILLION; i++) hs.has(i); + +}); + suite.add(`${MILLION.toLocaleString()} ObjKey set & get`, () => { const hm = new HashMap<[number, number], number>(); const objKeys:[number, number][] = [];