mirror of
https://github.com/zrwusa/data-structure-typed.git
synced 2024-11-23 12:54:04 +00:00
feat: Reimplement a more efficient HashMap and correctly rename the original HashMap as LinkedHashMap.
This commit is contained in:
parent
9a9571431c
commit
65949b7d19
|
@ -1728,7 +1728,7 @@ export class BinaryTree<V = any, N extends BinaryTreeNode<V, N> = BinaryTreeNode
|
|||
* Time complexity: O(n)
|
||||
* Space complexity: O(n)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Time complexity: O(n)
|
||||
* Space complexity: O(n)
|
||||
|
|
|
@ -7,9 +7,329 @@
|
|||
*/
|
||||
|
||||
import { isWeakKey, rangeCheck } from '../../utils';
|
||||
import { HashMapLinkedNode, HashMapOptions } from '../../types';
|
||||
import { HashMapLinkedNode, HashMapOptions, HashMapStoreItem } from '../../types';
|
||||
|
||||
export class HashMap<K = any, V = any> {
|
||||
protected _store: { [key: string]: HashMapStoreItem<K, V> } = {};
|
||||
protected _objMap: Map<object, V> = new Map();
|
||||
|
||||
/**
|
||||
* The constructor function initializes a new instance of a class with optional elements and options.
|
||||
* @param elements - The `elements` parameter is an iterable containing key-value pairs `[K, V]`. It
|
||||
* is optional and defaults to an empty array `[]`. This parameter is used to initialize the map with
|
||||
* key-value pairs.
|
||||
* @param [options] - The `options` parameter is an optional object that can contain additional
|
||||
* configuration options for the constructor. In this case, it has one property:
|
||||
*/
|
||||
constructor(elements: Iterable<[K, V]> = [], options?: {
|
||||
hashFn: (key: K) => string
|
||||
}) {
|
||||
if (options) {
|
||||
const { hashFn } = options;
|
||||
if (hashFn) {
|
||||
this._hashFn = hashFn;
|
||||
|
||||
}
|
||||
}
|
||||
if (elements) {
|
||||
this.setMany(elements);
|
||||
}
|
||||
}
|
||||
|
||||
protected _size = 0;
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.size === 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._store = {};
|
||||
this._objMap.clear();
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `set` function adds a key-value pair to a map-like data structure, incrementing the size if
|
||||
* the key is not already present.
|
||||
* @param {K} key - The key parameter is the key used to identify the value in the data structure. It
|
||||
* can be of any type, but if it is an object, it will be stored in a Map, otherwise it will be
|
||||
* stored in a regular JavaScript object.
|
||||
* @param {V} value - The value parameter represents the value that you want to associate with the
|
||||
* key in the data structure.
|
||||
*/
|
||||
set(key: K, value: V) {
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The function "setMany" sets multiple key-value pairs in a map.
|
||||
* @param elements - The `elements` parameter is an iterable containing key-value pairs. Each
|
||||
* key-value pair is represented as an array with two elements: the key and the value.
|
||||
*/
|
||||
setMany(elements: Iterable<[K, V]>) {
|
||||
for (const [key, value] of elements) this.set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The `get` function retrieves a value from a map based on a given key, either from an object map or
|
||||
* a string map.
|
||||
* @param {K} key - The `key` parameter is the key used to retrieve a value from the map. It can be
|
||||
* of any type, but it should be compatible with the key type used when the map was created.
|
||||
* @returns The method `get(key: K)` returns a value of type `V` if the key exists in the `_objMap`
|
||||
* or `_store`, otherwise it returns `undefined`.
|
||||
*/
|
||||
get(key: K): V | undefined {
|
||||
if (this._isObjKey(key)) {
|
||||
return this._objMap.get(key);
|
||||
} else {
|
||||
const strKey = this._getNoObjKey(key);
|
||||
return this._store[strKey]?.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `has` function checks if a given key exists in the `_objMap` or `_store` based on whether it
|
||||
* is an object key or not.
|
||||
* @param {K} key - The parameter "key" is of type K, which means it can be any type.
|
||||
* @returns The `has` method is returning a boolean value.
|
||||
*/
|
||||
has(key: K): boolean {
|
||||
if (this._isObjKey(key)) {
|
||||
return this._objMap.has(key);
|
||||
} else {
|
||||
const strKey = this._getNoObjKey(key);
|
||||
return strKey in this._store;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `delete` function removes an element from a map-like data structure based on the provided key.
|
||||
* @param {K} key - The `key` parameter is the key of the element that you want to delete from the
|
||||
* data structure.
|
||||
* @returns The `delete` method returns a boolean value. It returns `true` if the key was
|
||||
* successfully deleted from the map, and `false` if the key was not found in the map.
|
||||
*/
|
||||
delete(key: K): boolean {
|
||||
if (this._isObjKey(key)) {
|
||||
if (this._objMap.has(key)) {
|
||||
this._size--
|
||||
}
|
||||
|
||||
return this._objMap.delete(key);
|
||||
} else {
|
||||
const strKey = this._getNoObjKey(key);
|
||||
if (strKey in this._store) {
|
||||
delete this._store[strKey];
|
||||
this._size--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The function returns an iterator that yields key-value pairs from both an object store and an
|
||||
* object map.
|
||||
*/
|
||||
* [Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
for (const node of Object.values(this._store)) {
|
||||
yield [node.key, node.value] as [K, V];
|
||||
}
|
||||
for (const node of this._objMap) {
|
||||
yield node as [K, V];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The function returns an iterator that yields key-value pairs from the object.
|
||||
*/
|
||||
* entries(): IterableIterator<[K, V]> {
|
||||
for (const item of this) {
|
||||
yield item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The function `keys()` returns an iterator that yields all the keys of the object.
|
||||
*/
|
||||
* keys(): IterableIterator<K> {
|
||||
for (const [key] of this) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
|
||||
* values(): IterableIterator<V> {
|
||||
for (const [, value] of this) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `every` function checks if every element in a HashMap satisfies a given predicate function.
|
||||
* @param predicate - The predicate parameter is a function that takes four arguments: value, key,
|
||||
* index, and map. It is used to test each element in the map against a condition. If the predicate
|
||||
* function returns false for any element, the every() method will return false. If the predicate
|
||||
* function returns true for all
|
||||
* @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value
|
||||
* to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be
|
||||
* passed as the `this` value to the `predicate` function. If `thisArg` is
|
||||
* @returns The method is returning a boolean value. It returns true if the predicate function
|
||||
* returns true for every element in the map, and false otherwise.
|
||||
*/
|
||||
every(predicate: (value: V, key: K, index: number, map: HashMap<K, V>) => boolean, thisArg?: any): boolean {
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
if (!predicate.call(thisArg, value, key, index++, this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "some" function checks if at least one element in a HashMap satisfies a given predicate.
|
||||
* @param predicate - The `predicate` parameter is a function that takes four arguments: `value`,
|
||||
* `key`, `index`, and `map`. It is used to determine whether a specific condition is met for a given
|
||||
* key-value pair in the `HashMap`.
|
||||
* @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value
|
||||
* to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be
|
||||
* passed as the `this` value to the `predicate` function. If `thisArg` is
|
||||
* @returns a boolean value. It returns true if the predicate function returns true for any element
|
||||
* in the map, and false otherwise.
|
||||
*/
|
||||
some(predicate: (value: V, key: K, index: number, map: HashMap<K, V>) => boolean, thisArg?: any): boolean {
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
if (predicate.call(thisArg, value, key, index++, this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `forEach` function iterates over the elements of a HashMap and applies a callback function to
|
||||
* each element.
|
||||
* @param callbackfn - A function that will be called for each key-value pair in the HashMap. It
|
||||
* takes four parameters:
|
||||
* @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value
|
||||
* to be used as `this` when executing the `callbackfn` function. If `thisArg` is provided, it will
|
||||
* be passed as the `this` value inside the `callbackfn` function. If `thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: V, key: K, index: number, map: HashMap<K, V>) => void, thisArg?: any): void {
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
callbackfn.call(thisArg, value, key, index++, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `map` function in TypeScript creates a new HashMap by applying a callback function to each
|
||||
* key-value pair in the original HashMap.
|
||||
* @param callbackfn - The callback function that will be called for each key-value pair in the
|
||||
* HashMap. It takes four parameters:
|
||||
* @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value
|
||||
* to be used as `this` when executing the `callbackfn` function. If `thisArg` is provided, it will
|
||||
* be passed as the `this` value to the `callbackfn` function. If `thisArg
|
||||
* @returns The `map` method is returning a new `HashMap` object with the transformed values based on
|
||||
* the provided callback function.
|
||||
*/
|
||||
map<U>(callbackfn: (value: V, key: K, index: number, map: HashMap<K, V>) => U, thisArg?: any): HashMap<K, U> {
|
||||
const resultMap = new HashMap<K, U>();
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
resultMap.set(key, callbackfn.call(thisArg, value, key, index++, this));
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `filter` function creates a new HashMap containing key-value pairs from the original HashMap
|
||||
* that satisfy a given predicate function.
|
||||
* @param predicate - The predicate parameter is a function that takes four arguments: value, key,
|
||||
* index, and map. It is used to determine whether an element should be included in the filtered map
|
||||
* or not. The function should return a boolean value - true if the element should be included, and
|
||||
* false otherwise.
|
||||
* @param {any} [thisArg] - The `thisArg` parameter is an optional argument that specifies the value
|
||||
* to be used as `this` when executing the `predicate` function. If `thisArg` is provided, it will be
|
||||
* passed as the `this` value to the `predicate` function. If `thisArg` is
|
||||
* @returns The `filter` method is returning a new `HashMap` object that contains the key-value pairs
|
||||
* from the original `HashMap` that pass the provided `predicate` function.
|
||||
*/
|
||||
filter(predicate: (value: V, key: K, index: number, map: HashMap<K, V>) => boolean, thisArg?: any): HashMap<K, V> {
|
||||
const filteredMap = new HashMap<K, V>();
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
if (predicate.call(thisArg, value, key, index++, this)) {
|
||||
filteredMap.set(key, value);
|
||||
}
|
||||
}
|
||||
return filteredMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `reduce` function iterates over the elements of a HashMap and applies a callback function to
|
||||
* each element, accumulating a single value.
|
||||
* @param callbackfn - The callback function that will be called for each element in the HashMap. It
|
||||
* takes five parameters:
|
||||
* @param {U} initialValue - The initialValue parameter is the initial value of the accumulator. It
|
||||
* is the value that will be used as the first argument of the callback function when reducing the
|
||||
* elements of the map.
|
||||
* @returns The `reduce` method is returning the final value of the accumulator after iterating over
|
||||
* all the elements in the `HashMap`.
|
||||
*/
|
||||
reduce<U>(callbackfn: (accumulator: U, currentValue: V, currentKey: K, index: number, map: HashMap<K, V>) => U, initialValue: U): U {
|
||||
let accumulator = initialValue;
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
accumulator = callbackfn(accumulator, value, key, index++, this);
|
||||
}
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
protected _hashFn: (key: K) => string = (key: K) => String(key);
|
||||
|
||||
protected _isObjKey(key: any): key is (object | ((...args: any[]) => any)) {
|
||||
const keyType = typeof key;
|
||||
return (keyType === 'object' || keyType === 'function') && key !== null
|
||||
}
|
||||
|
||||
protected _getNoObjKey(key: K): string {
|
||||
const keyType = typeof key;
|
||||
|
||||
let strKey: string;
|
||||
if (keyType !== "string" && keyType !== "number" && keyType !== "symbol") {
|
||||
strKey = this._hashFn(key);
|
||||
} else {
|
||||
if (keyType === "number") {
|
||||
// TODO numeric key should has its own hash
|
||||
strKey = <string>key;
|
||||
} else {
|
||||
strKey = <string>key;
|
||||
}
|
||||
}
|
||||
return strKey;
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkedHashMap<K = any, V = any> {
|
||||
|
||||
protected _noObjMap: Record<string, HashMapLinkedNode<K, V | undefined>> = {};
|
||||
protected _objMap = new WeakMap<object, HashMapLinkedNode<K, V | undefined>>();
|
||||
|
@ -108,51 +428,78 @@ export class HashMap<K = any, V = any> {
|
|||
*/
|
||||
set(key: K, value?: V) {
|
||||
let node;
|
||||
const isNewKey = !this.has(key); // Check if the key is new
|
||||
|
||||
if (isWeakKey(key)) {
|
||||
const hash = this._objHashFn(key);
|
||||
node = this._objMap.get(hash);
|
||||
|
||||
if (node) {
|
||||
// If the node already exists, update its value
|
||||
node.value = value;
|
||||
} else {
|
||||
if (!node && isNewKey) {
|
||||
// Create new node
|
||||
node = { key: <K>hash, value, prev: this._tail, next: this._sentinel };
|
||||
|
||||
// Add new nodes to _objMap and linked list
|
||||
this._objMap.set(hash, node);
|
||||
} else if (node) {
|
||||
// Update the value of an existing node
|
||||
node.value = value;
|
||||
}
|
||||
} else {
|
||||
const hash = this._hashFn(key);
|
||||
// Non-object keys are handled in the same way as the original implementation
|
||||
node = this._noObjMap[hash];
|
||||
if (node) {
|
||||
|
||||
if (!node && isNewKey) {
|
||||
this._noObjMap[hash] = node = { key, value, prev: this._tail, next: this._sentinel };
|
||||
} else if (node) {
|
||||
// Update the value of an existing node
|
||||
node.value = value;
|
||||
} else {
|
||||
this._noObjMap[hash] = node = {
|
||||
key,
|
||||
value,
|
||||
prev: this._tail,
|
||||
next: this._sentinel
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this._size === 0) {
|
||||
this._head = node;
|
||||
this._sentinel.next = node;
|
||||
} else {
|
||||
this._tail.next = node;
|
||||
if (node && isNewKey) {
|
||||
// Update the head and tail of the linked list
|
||||
if (this._size === 0) {
|
||||
this._head = node;
|
||||
this._sentinel.next = node;
|
||||
} else {
|
||||
this._tail.next = node;
|
||||
node.prev = this._tail; // Make sure that the prev of the new node points to the current tail node
|
||||
}
|
||||
this._tail = node;
|
||||
this._sentinel.prev = node;
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._tail = node;
|
||||
this._sentinel.prev = node;
|
||||
this._size++;
|
||||
|
||||
return this._size;
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
if (isWeakKey(key)) {
|
||||
const hash = this._objHashFn(key);
|
||||
return this._objMap.has(hash);
|
||||
} else {
|
||||
const hash = this._hashFn(key);
|
||||
return hash in this._noObjMap;
|
||||
}
|
||||
}
|
||||
|
||||
setMany(entries: Iterable<[K, V]>): void {
|
||||
for (const entry of entries) {
|
||||
const [key, value] = entry;
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
keys(): K[] {
|
||||
const keys: K[] = [];
|
||||
for (const [key] of this) keys.push(key);
|
||||
return keys;
|
||||
}
|
||||
|
||||
values(): V[] {
|
||||
const values: V[] = [];
|
||||
for (const [, value] of this) values.push(value);
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time Complexity: O(1)
|
||||
* Space Complexity: O(1)
|
||||
|
@ -283,16 +630,25 @@ export class HashMap<K = any, V = any> {
|
|||
this._head = this._tail = this._sentinel.prev = this._sentinel.next = this._sentinel;
|
||||
}
|
||||
|
||||
clone(): LinkedHashMap<K, V> {
|
||||
const cloned = new LinkedHashMap<K, V>([], { hashFn: this._hashFn, objHashFn: this._objHashFn });
|
||||
for (const entry of this) {
|
||||
const [key, value] = entry;
|
||||
cloned.set(key, value);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time Complexity: O(n), where n is the number of elements in the HashMap.
|
||||
* Time Complexity: O(n), where n is the number of elements in the LinkedHashMap.
|
||||
* Space Complexity: O(1)
|
||||
*
|
||||
* The `forEach` function iterates over each element in a HashMap and executes a callback function on
|
||||
* The `forEach` function iterates over each element in a LinkedHashMap and executes a callback function on
|
||||
* each element.
|
||||
* @param callback - The callback parameter is a function that will be called for each element in the
|
||||
* HashMap. It takes three arguments:
|
||||
* LinkedHashMap. It takes three arguments:
|
||||
*/
|
||||
forEach(callback: (element: [K, V], index: number, hashMap: HashMap<K, V>) => void) {
|
||||
forEach(callback: (element: [K, V], index: number, hashMap: LinkedHashMap<K, V>) => void) {
|
||||
let index = 0;
|
||||
let node = this._head;
|
||||
while (node !== this._sentinel) {
|
||||
|
@ -302,15 +658,15 @@ export class HashMap<K = any, V = any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* The `filter` function takes a predicate function and returns a new HashMap containing only the
|
||||
* The `filter` function takes a predicate function and returns a new LinkedHashMap containing only the
|
||||
* key-value pairs that satisfy the predicate.
|
||||
* @param predicate - The `predicate` parameter is a function that takes two arguments: `element` and
|
||||
* `map`.
|
||||
* @returns a new HashMap object that contains the key-value pairs from the original HashMap that
|
||||
* @returns a new LinkedHashMap object that contains the key-value pairs from the original LinkedHashMap that
|
||||
* satisfy the given predicate function.
|
||||
*/
|
||||
filter(predicate: (element: [K, V], index: number, map: HashMap<K, V>) => boolean): HashMap<K, V> {
|
||||
const filteredMap = new HashMap<K, V>();
|
||||
filter(predicate: (element: [K, V], index: number, map: LinkedHashMap<K, V>) => boolean): LinkedHashMap<K, V> {
|
||||
const filteredMap = new LinkedHashMap<K, V>();
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
if (predicate([key, value], index, this)) {
|
||||
|
@ -322,14 +678,14 @@ export class HashMap<K = any, V = any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* The `map` function takes a callback function and returns a new HashMap with the values transformed
|
||||
* The `map` function takes a callback function and returns a new LinkedHashMap with the values transformed
|
||||
* by the callback.
|
||||
* @param callback - The `callback` parameter is a function that takes two arguments: `element` and
|
||||
* `map`.
|
||||
* @returns a new HashMap object with the values mapped according to the provided callback function.
|
||||
* @returns a new LinkedHashMap object with the values mapped according to the provided callback function.
|
||||
*/
|
||||
map<NV>(callback: (element: [K, V], index: number, map: HashMap<K, V>) => NV): HashMap<K, NV> {
|
||||
const mappedMap = new HashMap<K, NV>();
|
||||
map<NV>(callback: (element: [K, V], index: number, map: LinkedHashMap<K, V>) => NV): LinkedHashMap<K, NV> {
|
||||
const mappedMap = new LinkedHashMap<K, NV>();
|
||||
let index = 0;
|
||||
for (const [key, value] of this) {
|
||||
const newValue = callback([key, value], index, this);
|
||||
|
@ -340,18 +696,18 @@ export class HashMap<K = any, V = any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* The `reduce` function iterates over the elements of a HashMap and applies a callback function to
|
||||
* The `reduce` function iterates over the elements of a LinkedHashMap and applies a callback function to
|
||||
* each element, accumulating a single value.
|
||||
* @param callback - The callback parameter is a function that takes three arguments: accumulator,
|
||||
* element, and map. It is called for each element in the HashMap and is used to accumulate a single
|
||||
* element, and map. It is called for each element in the LinkedHashMap and is used to accumulate a single
|
||||
* result.
|
||||
* @param {A} initialValue - The `initialValue` parameter is the initial value of the accumulator. It
|
||||
* is the value that will be passed as the first argument to the `callback` function when reducing
|
||||
* the elements of the map.
|
||||
* @returns The `reduce` function is returning the final value of the accumulator after iterating
|
||||
* over all the elements in the HashMap and applying the callback function to each element.
|
||||
* over all the elements in the LinkedHashMap and applying the callback function to each element.
|
||||
*/
|
||||
reduce<A>(callback: (accumulator: A, element: [K, V], index: number, map: HashMap<K, V>) => A, initialValue: A): A {
|
||||
reduce<A>(callback: (accumulator: A, element: [K, V], index: number, map: LinkedHashMap<K, V>) => A, initialValue: A): A {
|
||||
let accumulator = initialValue;
|
||||
let index = 0;
|
||||
for (const entry of this) {
|
||||
|
@ -362,7 +718,7 @@ export class HashMap<K = any, V = any> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Time Complexity: O(n), where n is the number of elements in the HashMap.
|
||||
* Time Complexity: O(n), where n is the number of elements in the LinkedHashMap.
|
||||
* Space Complexity: O(1)
|
||||
*
|
||||
* The above function is an iterator that yields key-value pairs from a linked list.
|
||||
|
|
|
@ -9,3 +9,5 @@ export type HashMapOptions<K> = {
|
|||
hashFn: (key: K) => string;
|
||||
objHashFn: (key: K) => object
|
||||
}
|
||||
|
||||
export type HashMapStoreItem<K, V> = { key: K, value: V };
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { HashMap } from '../../../../src';
|
||||
import { HashMap, LinkedHashMap } from '../../../../src';
|
||||
import { getRandomInt, getRandomIntArray } from '../../../utils';
|
||||
|
||||
describe('HashMap', () => {
|
||||
describe('HashMap Test1', () => {
|
||||
let hashMap: HashMap<string, number>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -10,10 +10,6 @@ describe('HashMap', () => {
|
|||
|
||||
it('should initialize correctly', () => {
|
||||
expect(hashMap.size).toBe(0);
|
||||
// expect(hashMap.table.length).toBe(16);
|
||||
// expect(hashMap.loadFactor).toBe(0.75);
|
||||
// expect(hashMap.capacityMultiplier).toBe(2);
|
||||
// expect(hashMap.initialCapacity).toBe(16);
|
||||
expect(hashMap.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -59,10 +55,6 @@ describe('HashMap', () => {
|
|||
hashMap.set('two', 2);
|
||||
hashMap.set('three', 3);
|
||||
|
||||
// const entries = Array.from(hashMap.entries());
|
||||
// expect(entries).toContainEqual(['one', 1]);
|
||||
// expect(entries).toContainEqual(['two', 2]);
|
||||
// expect(entries).toContainEqual(['three', 3]);
|
||||
});
|
||||
|
||||
it('should resize the table when load factor is exceeded', () => {
|
||||
|
@ -95,7 +87,7 @@ describe('HashMap', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('HashMap', () => {
|
||||
describe('HashMap Test2', () => {
|
||||
let hashMap: HashMap;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -117,6 +109,297 @@ describe('HashMap', () => {
|
|||
expect(hashMap.get(keyObj)).toBe('objectValue');
|
||||
});
|
||||
|
||||
// it('should handle number keys correctly', () => {
|
||||
// hashMap.set(999, { a: '999Value' });
|
||||
// hashMap.set('999', {a: '999StrValue'})
|
||||
// expect(hashMap.get(999)).toEqual({ a: '999Value' });
|
||||
// expect(hashMap.get('999')).toEqual({ a: '999StrValue' });
|
||||
// });
|
||||
|
||||
it('should update the value for an existing key', () => {
|
||||
hashMap.set('key1', 'value1');
|
||||
hashMap.set('key1', 'newValue');
|
||||
expect(hashMap.get('key1')).toBe('newValue');
|
||||
});
|
||||
|
||||
it('should return undefined for a non-existent key', () => {
|
||||
expect(hashMap.get('nonExistentKey')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove a key-value pair', () => {
|
||||
hashMap.set('key1', 'value1');
|
||||
hashMap.delete('key1');
|
||||
expect(hashMap.get('key1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear the map', () => {
|
||||
hashMap.set('key1', 'value1');
|
||||
expect(hashMap.size).toBe(1);
|
||||
|
||||
hashMap.clear();
|
||||
expect(hashMap.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should iterate over values', () => {
|
||||
hashMap.set('key1', 'value1');
|
||||
hashMap.set('key2', 'value2');
|
||||
const values = [];
|
||||
for (const value of hashMap) {
|
||||
values.push(value);
|
||||
}
|
||||
expect(values).toEqual([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2']
|
||||
]);
|
||||
});
|
||||
|
||||
function compareHashMaps(hashMap: HashMap<unknown, unknown>, stdMap: Map<unknown, unknown>) {
|
||||
expect(hashMap.size).toEqual(stdMap.size);
|
||||
stdMap.forEach((value, key) => {
|
||||
expect(hashMap.get(key)).toEqual(value);
|
||||
});
|
||||
}
|
||||
|
||||
const stdMap: Map<unknown, unknown> = new Map();
|
||||
const arr: number[] = getRandomIntArray(1000, 1, 10000);
|
||||
|
||||
it('delete test', () => {
|
||||
for (const item of arr) {
|
||||
stdMap.set(item, item);
|
||||
hashMap.set(item, item);
|
||||
}
|
||||
for (const item of arr) {
|
||||
if (Math.random() > 0.6) {
|
||||
expect(hashMap.delete(item)).toEqual(stdMap.delete(item));
|
||||
}
|
||||
}
|
||||
compareHashMaps(hashMap, stdMap);
|
||||
|
||||
for (let i = 0; i < 1000; ++i) {
|
||||
const random = getRandomInt(0, 100);
|
||||
expect(hashMap.delete(random)).toEqual(stdMap.delete(random));
|
||||
}
|
||||
compareHashMaps(hashMap, stdMap);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('HashMap for coordinate object keys', () => {
|
||||
const hashMap: HashMap<[number, number], number> = new HashMap();
|
||||
const codObjs: [number, number][] = [];
|
||||
|
||||
test('set elements in hash map', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const codObj: [number, number] = [getRandomInt(-10000, 10000), i];
|
||||
codObjs.push(codObj);
|
||||
hashMap.set(codObj, i);
|
||||
}
|
||||
});
|
||||
|
||||
test('get elements in hash map', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const codObj = codObjs[i];
|
||||
if (codObj) {
|
||||
expect(hashMap.get(codObj)).toBe(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('delete elements in hash map', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (i === 500) expect(hashMap.size).toBe(500)
|
||||
const codObj = codObjs[i];
|
||||
if (codObj) hashMap.delete(codObj);
|
||||
}
|
||||
expect(hashMap.size).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('HashMap setMany, keys, values', () => {
|
||||
const hm: HashMap<number, number> = new HashMap<number, number>();
|
||||
|
||||
beforeEach(() => {
|
||||
hm.clear()
|
||||
hm.setMany([[2, 2], [3, 3], [4, 4], [5, 5]])
|
||||
hm.setMany([[2, 2], [3, 3], [4, 4], [6, 6]])
|
||||
})
|
||||
|
||||
test('keys', () => {
|
||||
expect([...hm.keys()]).toEqual([2, 3, 4, 5, 6])
|
||||
});
|
||||
|
||||
test('values', () => {
|
||||
expect([...hm.values()]).toEqual([2, 3, 4, 5, 6])
|
||||
});
|
||||
});
|
||||
|
||||
describe('HashMap HOF', () => {
|
||||
let hashMap: HashMap;
|
||||
|
||||
beforeEach(() => {
|
||||
hashMap = new HashMap<string, string>();
|
||||
hashMap.set('key1', 'value1');
|
||||
hashMap.set('key2', 'value2');
|
||||
hashMap.set('key3', 'value3');
|
||||
});
|
||||
|
||||
test('every() returns true if all elements match the condition', () => {
|
||||
expect(hashMap.every((value, key) => typeof value === 'string')).toBe(true);
|
||||
});
|
||||
|
||||
test('some() returns true if any element matches the condition', () => {
|
||||
expect(hashMap.some((value, key) => key === 'key1')).toBe(true);
|
||||
});
|
||||
|
||||
test('forEach() should execute a function for each element', () => {
|
||||
const mockCallback = jest.fn();
|
||||
hashMap.forEach(mockCallback);
|
||||
expect(mockCallback.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
test('map() should transform each element', () => {
|
||||
const newHashMap = hashMap.map((value, key) => value.toUpperCase());
|
||||
expect(newHashMap.get('key1')).toBe('VALUE1');
|
||||
});
|
||||
|
||||
test('filter() should remove elements that do not match the condition', () => {
|
||||
const filteredHashMap = hashMap.filter((value, key) => key !== 'key1');
|
||||
expect(filteredHashMap.has('key1')).toBe(false);
|
||||
});
|
||||
|
||||
test('reduce() should accumulate values', () => {
|
||||
const result = hashMap.reduce((acc, value, key) => acc + value, '');
|
||||
expect(result).toBe('value1value2value3');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('LinkedHashMap Test1', () => {
|
||||
let hashMap: LinkedHashMap<string, number>;
|
||||
|
||||
beforeEach(() => {
|
||||
hashMap = new LinkedHashMap<string, number>();
|
||||
});
|
||||
|
||||
it('should initialize correctly', () => {
|
||||
expect(hashMap.size).toBe(0);
|
||||
// expect(hashMap.table.length).toBe(16);
|
||||
// expect(hashMap.loadFactor).toBe(0.75);
|
||||
// expect(hashMap.capacityMultiplier).toBe(2);
|
||||
// expect(hashMap.initialCapacity).toBe(16);
|
||||
expect(hashMap.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should put and get values', () => {
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
hashMap.set('three', 3);
|
||||
|
||||
expect(hashMap.get('one')).toBe(1);
|
||||
expect(hashMap.get('two')).toBe(2);
|
||||
expect(hashMap.get('three')).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle key collisions', () => {
|
||||
// Force a collision by setting two different keys to the same bucket
|
||||
hashMap.set('key1', 1);
|
||||
hashMap.set('key2', 2);
|
||||
|
||||
expect(hashMap.get('key1')).toBe(1);
|
||||
expect(hashMap.get('key2')).toBe(2);
|
||||
});
|
||||
|
||||
it('should delete values', () => {
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
|
||||
hashMap.delete('one');
|
||||
expect(hashMap.get('one')).toBeUndefined();
|
||||
expect(hashMap.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear the LinkedHashMap', () => {
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
|
||||
hashMap.clear();
|
||||
expect(hashMap.size).toBe(0);
|
||||
expect(hashMap.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should iterate over entries', () => {
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
hashMap.set('three', 3);
|
||||
|
||||
// const entries = Array.from(hashMap.entries());
|
||||
// expect(entries).toContainEqual(['one', 1]);
|
||||
// expect(entries).toContainEqual(['two', 2]);
|
||||
// expect(entries).toContainEqual(['three', 3]);
|
||||
});
|
||||
|
||||
it('should resize the table when load factor is exceeded', () => {
|
||||
// Set a small initial capacity for testing resizing
|
||||
hashMap = new LinkedHashMap<string, number>();
|
||||
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
hashMap.set('three', 3);
|
||||
hashMap.set('four', 4); // This should trigger a resize
|
||||
|
||||
// expect(hashMap.table.length).toBe(8);
|
||||
expect(hashMap.get('one')).toBe(1);
|
||||
expect(hashMap.get('two')).toBe(2);
|
||||
expect(hashMap.get('three')).toBe(3);
|
||||
expect(hashMap.get('four')).toBe(4);
|
||||
});
|
||||
|
||||
it('should allow using a custom hash function', () => {
|
||||
hashMap = new LinkedHashMap<string, number>();
|
||||
|
||||
hashMap.set('one', 1);
|
||||
hashMap.set('two', 2);
|
||||
|
||||
expect(hashMap.get('one')).toBe(1);
|
||||
expect(hashMap.get('two')).toBe(2);
|
||||
// Since the custom hash function always returns 0, these keys will collide.
|
||||
// Make sure they are stored separately.
|
||||
// expect(hashMap.table[0].length).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
// it('should handle number keys correctly', () => {
|
||||
// const hm = new LinkedHashMap();
|
||||
// hm.set(999, { a: '999Value' });
|
||||
// hm.set('999', {a: '999StrValue'})
|
||||
// expect(hm.get(999)).toEqual({ a: '999Value' });
|
||||
// expect(hm.get('999')).toEqual({ a: '999StrValue1' });
|
||||
// });
|
||||
});
|
||||
|
||||
describe('LinkedHashMap Test2', () => {
|
||||
let hashMap: LinkedHashMap;
|
||||
|
||||
beforeEach(() => {
|
||||
hashMap = new LinkedHashMap();
|
||||
});
|
||||
|
||||
it('should create an empty map', () => {
|
||||
expect(hashMap.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should add a key-value pair', () => {
|
||||
hashMap.set('key1', 'value1');
|
||||
expect(hashMap.get('key1')).toBe('value1');
|
||||
});
|
||||
|
||||
it('should handle object keys correctly', () => {
|
||||
const keyObj = { id: 1 };
|
||||
hashMap.set(keyObj, 'objectValue');
|
||||
expect(hashMap.get(keyObj)).toBe('objectValue');
|
||||
});
|
||||
|
||||
it('should handle number keys correctly', () => {
|
||||
hashMap.set(999, { a: '999Value' });
|
||||
expect(hashMap.get(999)).toEqual({ a: '999Value' });
|
||||
|
@ -159,14 +442,7 @@ describe('HashMap', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
// test('should delete element at specific index', () => {
|
||||
// hashMap.set('key1', 'value1');
|
||||
// hashMap.set('key2', 'value2');
|
||||
// hashMap.deleteAt(0);
|
||||
// expect(hashMap.get('key1')).toBeUndefined();
|
||||
// expect(hashMap.size).toBe(1);
|
||||
// });
|
||||
function compareHashMaps(hashMap: HashMap<unknown, unknown>, stdMap: Map<unknown, unknown>) {
|
||||
function compareHashMaps(hashMap: LinkedHashMap<unknown, unknown>, stdMap: Map<unknown, unknown>) {
|
||||
expect(hashMap.size).toEqual(stdMap.size);
|
||||
let index = 0;
|
||||
stdMap.forEach((value, key) => {
|
||||
|
@ -230,8 +506,8 @@ describe('HashMap', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('HashMap for coordinate object keys', () => {
|
||||
const hashMap: HashMap<[number, number], number> = new HashMap();
|
||||
describe('LinkedHashMap for coordinate object keys', () => {
|
||||
const hashMap: LinkedHashMap<[number, number], number> = new LinkedHashMap();
|
||||
const codObjs: [number, number][] = [];
|
||||
|
||||
test('set elements in hash map', () => {
|
||||
|
@ -261,3 +537,21 @@ describe('HashMap for coordinate object keys', () => {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
describe('LinkedHashMap setMany, keys, values', () => {
|
||||
const hm: LinkedHashMap<number, number> = new LinkedHashMap<number, number>();
|
||||
|
||||
beforeEach(() => {
|
||||
hm.clear()
|
||||
hm.setMany([[2, 2], [3, 3], [4, 4], [5, 5]])
|
||||
hm.setMany([[2, 2], [3, 3], [4, 4], [6, 6]])
|
||||
})
|
||||
|
||||
test('keys', () => {
|
||||
expect(hm.keys()).toEqual([2, 3, 4, 5, 6])
|
||||
});
|
||||
|
||||
test('values', () => {
|
||||
expect(hm.values()).toEqual([2, 3, 4, 5, 6])
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue