diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4730d3c..b48c50c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: paths-ignore: - 'lib/**' - 'dist/**' + - 'umd/**' - 'docs/**' - 'coverage/**' - '**/*.md' diff --git a/README.md b/README.md index 98e4174..77b760e 100644 --- a/README.md +++ b/README.md @@ -441,24 +441,17 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D Set std::set<T> - Set + Set<E> HashSet<E> set Map std::map<K, V> - Map + Map<K, V> HashMap<K, V> dict - - Unordered Map - std::unordered_map<K, V> - N/A - HashMap<K, V> - defaultdict - Unordered Set std::unordered_set<T> @@ -466,24 +459,31 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D HashSet<E> N/A + + Unordered Map + std::unordered_map<K, V> + HashTable<K, V> + HashMap<K, V> + defaultdict + Queue std::queue<T> - Queue + Queue<E> Queue<E> N/A Priority Queue std::priority_queue<T> - PriorityQueue + PriorityQueue<E> PriorityQueue<E> N/A Stack std::stack<T> - Stack + Stack<E> Stack<E> N/A @@ -497,7 +497,7 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D Deque std::deque<T> - Deque + Deque<E> N/A N/A @@ -525,14 +525,14 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D Ordered Dictionary N/A - Map + Map<K, V> N/A OrderedDict Double-Ended Queue (Deque) std::deque<T> - Deque + Deque<E> N/A N/A diff --git a/src/data-structures/binary-tree/segment-tree.ts b/src/data-structures/binary-tree/segment-tree.ts index 4dda8dd..be66103 100644 --- a/src/data-structures/binary-tree/segment-tree.ts +++ b/src/data-structures/binary-tree/segment-tree.ts @@ -87,12 +87,21 @@ export class SegmentTree { * included in the range. If not provided, it defaults to the index of the last element in the "values" array. */ constructor(values: number[], start?: number, end?: number) { + console.log('values.length:', values.length); start = start || 0; end = end || values.length - 1; this._values = values; this._start = start; this._end = end; - this._root = this.build(start, end); + + if (values.length > 0) { + console.log('Initializing with non-empty array'); + this._root = this.build(start, end); + } else { + console.log('Initializing with empty array'); + this._root = null; + this._values = []; + } } private _values: number[] = []; @@ -119,15 +128,18 @@ export class SegmentTree { } /** - * The function builds a segment tree by recursively dividing the given range into smaller segments and creating nodes - * for each segment. + * The build function creates a segment tree by recursively dividing the given range into smaller segments and assigning + * the sum of values to each segment. * @param {number} start - The `start` parameter represents the starting index of the segment or range for which we are * building the segment tree. - * @param {number} end - The `end` parameter represents the ending index of the segment or range for which we are - * building the segment tree. + * @param {number} end - The "end" parameter represents the ending index of the segment or range for which we want to + * build a segment tree. * @returns a SegmentTreeNode object. */ build(start: number, end: number): SegmentTreeNode { + if (start > end) { + return new SegmentTreeNode(start, end, 0); + } if (start === end) return new SegmentTreeNode(start, end, this._values[start]); const mid = start + Math.floor((end - start) / 2); @@ -192,33 +204,39 @@ export class SegmentTree { return 0; } + if (indexA < 0 || indexB >= this.values.length || indexA > indexB) { + return NaN; + } + const dfs = (cur: SegmentTreeNode, i: number, j: number): number => { - if (cur.start === i && cur.end === j) { + if (i <= cur.start && j >= cur.end) { + // The range [i, j] completely covers the current node's range [cur.start, cur.end] return cur.sum; } const mid = cur.start + Math.floor((cur.end - cur.start) / 2); if (j <= mid) { - // TODO after no-non-null-assertion not ensure the logic if (cur.left) { return dfs(cur.left, i, j); } else { return NaN; } } else if (i > mid) { - // TODO after no-non-null-assertion not ensure the logic if (cur.right) { - // TODO after no-non-null-assertion not ensure the logic return dfs(cur.right, i, j); } else { return NaN; } } else { - // TODO after no-non-null-assertion not ensure the logic - if (cur.left && cur.right) { - return dfs(cur.left, i, mid) + dfs(cur.right, mid + 1, j); - } else { - return NaN; + // Query both left and right subtrees + let leftSum = 0; + let rightSum = 0; + if (cur.left) { + leftSum = dfs(cur.left, i, mid); } + if (cur.right) { + rightSum = dfs(cur.right, mid + 1, j); + } + return leftSum + rightSum; } }; return dfs(root, indexA, indexB); diff --git a/src/data-structures/hash/hash-map.ts b/src/data-structures/hash/hash-map.ts new file mode 100644 index 0000000..7dc52fe --- /dev/null +++ b/src/data-structures/hash/hash-map.ts @@ -0,0 +1,203 @@ +import {HashFunction} from '../../types'; + +/** + * data-structure-typed + * + * @author Tyler Zeng + * @copyright Copyright (c) 2022 Tyler Zeng + * @license MIT License + */ +export class HashMap { + get hashFn(): HashFunction { + return this._hashFn; + } + + set hashFn(value: HashFunction) { + this._hashFn = value; + } + get table(): Array> { + return this._table; + } + + set table(value: Array>) { + this._table = value; + } + + get capacityMultiplier(): number { + return this._capacityMultiplier; + } + + set capacityMultiplier(value: number) { + this._capacityMultiplier = value; + } + + get loadFactor(): number { + return this._loadFactor; + } + + set loadFactor(value: number) { + this._loadFactor = value; + } + + get initialCapacity(): number { + return this._initialCapacity; + } + + set initialCapacity(value: number) { + this._initialCapacity = value; + } + + get size(): number { + return this._size; + } + + set size(value: number) { + this._size = value; + } + + private _initialCapacity: number; + private _loadFactor: number; + private _capacityMultiplier: number; + private _size: number; + private _table: Array>; + private _hashFn: HashFunction; + + /** + * The constructor initializes the properties of a hash table, including the initial capacity, load factor, capacity + * multiplier, size, table array, and hash function. + * @param [initialCapacity=16] - The initial capacity is the initial size of the hash table. It determines the number of + * buckets or slots available for storing key-value pairs. The default value is 16. + * @param [loadFactor=0.75] - The load factor is a measure of how full the hash table can be before it is resized. It is + * a value between 0 and 1, where 1 means the hash table is completely full and 0 means it is completely empty. When the + * load factor is reached, the hash table will + * @param [hashFn] - The `hashFn` parameter is an optional parameter that represents the hash function used to calculate + * the index of a key in the hash table. If a custom hash function is not provided, a default hash function is used. The + * default hash function converts the key to a string, calculates the sum of the + */ + constructor(initialCapacity = 16, loadFactor = 0.75, hashFn?: HashFunction) { + this._initialCapacity = initialCapacity; + this._loadFactor = loadFactor; + this._capacityMultiplier = 2; + this._size = 0; + this._table = new Array(initialCapacity); + this._hashFn = + hashFn || + ((key: K) => { + const strKey = String(key); + let hash = 0; + for (let i = 0; i < strKey.length; i++) { + hash += strKey.charCodeAt(i); + } + return hash % this.table.length; + }); + } + + private _hash(key: K): number { + return this._hashFn(key); + } + + /** + * The `resizeTable` function resizes the table used in a hash map by creating a new table with a specified capacity and + * rehashing the key-value pairs from the old table into the new table. + * @param {number} newCapacity - The newCapacity parameter is the desired capacity for the resized table. It represents + * the number of buckets that the new table should have. + */ + private resizeTable(newCapacity: number): void { + const newTable = new Array(newCapacity); + for (const bucket of this._table) { + // Note that this is this._table + if (bucket) { + for (const [key, value] of bucket) { + const newIndex = this._hash(key) % newCapacity; + if (!newTable[newIndex]) { + newTable[newIndex] = []; + } + newTable[newIndex].push([key, value]); + } + } + } + this._table = newTable; // Again, here is this._table + } + + set(key: K, value: V): void { + const loadFactor = this.size / this.table.length; + if (loadFactor >= this.loadFactor) { + this.resizeTable(this.table.length * this.capacityMultiplier); + } + + const index = this._hash(key); + if (!this.table[index]) { + this.table[index] = []; + } + + // Check if the key already exists in the bucket + for (let i = 0; i < this.table[index].length; i++) { + if (this.table[index][i][0] === key) { + this.table[index][i][1] = value; + return; + } + } + + this.table[index].push([key, value]); + this.size++; + } + + get(key: K): V | undefined { + const index = this._hash(key); + if (!this.table[index]) { + return undefined; + } + + for (const [k, v] of this.table[index]) { + if (k === key) { + return v; + } + } + + return undefined; + } + + remove(key: K): void { + const index = this._hash(key); + if (!this.table[index]) { + return; + } + + for (let i = 0; i < this.table[index].length; i++) { + if (this.table[index][i][0] === key) { + this.table[index].splice(i, 1); + this.size--; + + // Check if the table needs to be resized down + const loadFactor = this.size / this.table.length; + if (loadFactor < this.loadFactor / this.capacityMultiplier) { + this.resizeTable(this.table.length / this.capacityMultiplier); + } + return; + } + } + } + + *entries(): IterableIterator<[K, V]> { + for (const bucket of this.table) { + if (bucket) { + for (const [key, value] of bucket) { + yield [key, value]; + } + } + } + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + clear(): void { + this.size = 0; + this.table = new Array(this.initialCapacity); + } + + isEmpty(): boolean { + return this.size === 0; + } +} diff --git a/src/data-structures/hash/hash-table.ts b/src/data-structures/hash/hash-table.ts index 6c177c6..21e38e9 100644 --- a/src/data-structures/hash/hash-table.ts +++ b/src/data-structures/hash/hash-table.ts @@ -5,10 +5,11 @@ * @copyright Copyright (c) 2022 Tyler Zeng * @license MIT License */ -export class HashNode { + +export class HashTableNode { key: K; val: V; - next: HashNode | null; + next: HashTableNode | null; constructor(key: K, val: V) { this.key = key; @@ -17,23 +18,25 @@ export class HashNode { } } +import {HashFunction} from '../../types'; + export class HashTable { - get buckets(): Array | null> { + get hashFn(): HashFunction { + return this._hashFn; + } + + set hashFn(value: HashFunction) { + this._hashFn = value; + } + + get buckets(): Array | null> { return this._buckets; } - set buckets(value: Array | null>) { + set buckets(value: Array | null>) { this._buckets = value; } - get size(): number { - return this._size; - } - - set size(value: number) { - this._size = value; - } - get capacity(): number { return this._capacity; } @@ -42,82 +45,163 @@ export class HashTable { this._capacity = value; } + private static readonly DEFAULT_CAPACITY = 16; + private static readonly LOAD_FACTOR = 0.75; + private _capacity: number; private _size: number; - private _buckets: Array | null>; + private _buckets: Array | null>; + private _hashFn: HashFunction; - /** - * The constructor initializes the capacity, size, and buckets of an object. - * @param [capacity=1000] - The `capacity` parameter represents the maximum number of elements that the data structure - * can hold. It is an optional parameter with a default value of 1000. - */ - constructor(capacity = 1000) { - this._capacity = capacity; + constructor(capacity: number = HashTable.DEFAULT_CAPACITY, hashFn?: HashFunction) { + this._hashFn = hashFn || this._defaultHashFn; + this._capacity = Math.max(capacity, HashTable.DEFAULT_CAPACITY); this._size = 0; - this._buckets = new Array(this.capacity).fill(null); + this._buckets = new Array | null>(this._capacity).fill(null); } /** - * The hash function takes a key, converts it to a string, calculates the sum of the ASCII values of its characters, and - * returns the remainder when divided by the capacity of the data structure. - * @param {K} key - The `key` parameter represents the key that needs to be hashed. It is of type `K`, which means it can - * be any data type that can be converted to a string. - * @returns The hash value of the key modulo the capacity of the data structure. + * The function `_defaultHashFn` calculates the hash value of a given key and returns the remainder when divided by the + * capacity of the data structure. + * @param {K} key - The `key` parameter is the input value that needs to be hashed. It can be of any type, but in this + * code snippet, it is checked whether the key is a string or an object. If it is a string, the `_murmurStringHashFn` + * function is used to + * @returns the hash value of the key modulo the capacity of the data structure. */ - private hash(key: K): number { + protected _defaultHashFn(key: K): number { + // Can be replaced with other hash functions as needed + const hashValue = typeof key === 'string' ? this._murmurStringHashFn(key) : this._objectHash(key); + return hashValue % this._capacity; + } + + /** + * The `_multiplicativeStringHashFn` function calculates a hash value for a given string key using the multiplicative + * string hash function. + * @param {K} key - The `key` parameter is the input value for which we want to calculate the hash. It can be of any + * type, as it is generic (`K`). The function converts the `key` to a string using the `String()` function. + * @returns a number, which is the result of the multiplicative string hash function applied to the input key. + */ + protected _multiplicativeStringHashFn(key: K): number { const keyString = String(key); let hash = 0; for (let i = 0; i < keyString.length; i++) { - hash += keyString.charCodeAt(i); + const charCode = keyString.charCodeAt(i); + // Some constants for adjusting the hash function + const A = 0.618033988749895; + const M = 1 << 30; // 2^30 + hash = (hash * A + charCode) % M; } - return hash % this.capacity; + return Math.abs(hash); // Take absolute value to ensure non-negative numbers } /** - * The put function adds a key-value pair to a hash table, handling collisions by chaining. - * @param {K} key - The key parameter represents the key of the key-value pair that you want to insert into the hash - * table. It is of type K, which can be any data type that can be used as a key, such as a string, number, or object. - * @param {V} val - The `val` parameter represents the value associated with the key in the hash table. - * @returns Nothing is being returned. The return type of the function is void, which means it does not return any value. + * The function `_murmurStringHashFn` calculates a hash value for a given string key using the MurmurHash algorithm. + * @param {K} key - The `key` parameter is the input value for which you want to calculate the hash. It can be of any + * type, but it will be converted to a string using the `String()` function before calculating the hash. + * @returns a number, which is the hash value calculated for the given key. */ - put(key: K, val: V): void { - const index = this.hash(key); - const newNode = new HashNode(key, val); + protected _murmurStringHashFn(key: K): number { + const keyString = String(key); + const seed = 0; + let hash = seed; - if (!this.buckets[index]) { - this.buckets[index] = newNode; + for (let i = 0; i < keyString.length; i++) { + const char = keyString.charCodeAt(i); + hash = (hash ^ char) * 0x5bd1e995; + hash = (hash ^ (hash >>> 15)) * 0x27d4eb2d; + hash = hash ^ (hash >>> 15); + } + + return Math.abs(hash); + } + + /** + * The _hash function takes a key and returns a number. + * @param {K} key - The parameter "key" is of type K, which represents the type of the key that will be hashed. + * @returns The hash function is returning a number. + */ + protected _hash(key: K): number { + return this.hashFn(key); + } + + /** + * The function calculates a hash value for a given string using the djb2 algorithm. + * @param {string} key - The `key` parameter in the `stringHash` function is a string value that represents the input for + * which we want to calculate the hash value. + * @returns a number, which is the hash value of the input string. + */ + protected _stringHash(key: string): number { + let hash = 0; + for (let i = 0; i < key.length; i++) { + hash = (hash * 31 + key.charCodeAt(i)) & 0xffffffff; + } + return hash; + } + + /** + * The function `_objectHash` takes a key and returns a hash value, using a custom hash function for objects. + * @param {K} key - The parameter "key" is of type "K", which means it can be any type. It could be a string, number, + * boolean, object, or any other type of value. The purpose of the objectHash function is to generate a hash value for + * the key, which can be used for + * @returns a number, which is the hash value of the key. + */ + protected _objectHash(key: K): number { + // If the key is an object, you can write a custom hash function + // For example, convert the object's properties to a string and use string hashing + // This is just an example; you should write a specific object hash function as needed + return this._stringHash(JSON.stringify(key)); + } + + /** + * The set function adds a key-value pair to the hash table, handling collisions and resizing if necessary. + * @param {K} key - The key parameter represents the key of the key-value pair that you want to insert into the hash + * table. It is of type K, which is a generic type representing the key's data type. + * @param {V} val - The parameter `val` represents the value that you want to associate with the given key in the hash + * table. + * @returns Nothing is being returned. The return type of the `put` method is `void`, which means it does not return any + * value. + */ + set(key: K, val: V): void { + const index = this._hash(key); + const newNode = new HashTableNode(key, val); + + if (!this._buckets[index]) { + this._buckets[index] = newNode; } else { - // Handle collision by chaining - let currentNode = this.buckets[index]!; - while (currentNode.next) { + // Handle collisions, consider using open addressing, etc. + let currentNode = this._buckets[index]!; + while (currentNode) { if (currentNode.key === key) { - // Update the val if the key already exists + // If the key already exists, update the value currentNode.val = val; return; } + if (!currentNode.next) { + break; + } currentNode = currentNode.next; } - if (currentNode.key === key) { - // Update the val if the key already exists (last node) - currentNode.val = val; - } else { - // Add the new node to the end of the chain - currentNode.next = newNode; - } + // Add to the end of the linked list + currentNode.next = newNode; + } + this._size++; + + // If the load factor is too high, resize the hash table + if (this._size / this._capacity >= HashTable.LOAD_FACTOR) { + this._expand(); } - this.size++; } /** * The `get` function retrieves the value associated with a given key from a hash table. - * @param {K} key - The parameter "key" represents the key of the element that we want to retrieve from the data + * @param {K} key - The `key` parameter represents the key of the element that we want to retrieve from the data * structure. * @returns The method is returning the value associated with the given key if it exists in the hash table. If the key is * not found, it returns `undefined`. */ get(key: K): V | undefined { - const index = this.hash(key); - let currentNode = this.buckets[index]; + const index = this._hash(key); + let currentNode = this._buckets[index]; while (currentNode) { if (currentNode.key === key) { @@ -129,29 +213,65 @@ export class HashTable { } /** - * The `remove` function removes a key-value pair from a hash table. + * The remove function removes a key-value pair from a hash table. * @param {K} key - The `key` parameter represents the key of the key-value pair that needs to be removed from the hash * table. * @returns Nothing is being returned. The `remove` method has a return type of `void`, which means it does not return * any value. */ remove(key: K): void { - const index = this.hash(key); - let currentNode = this.buckets[index]; - let prevNode: HashNode | null = null; + const index = this._hash(key); + let currentNode = this._buckets[index]; + let prevNode: HashTableNode | null = null; while (currentNode) { if (currentNode.key === key) { if (prevNode) { prevNode.next = currentNode.next; } else { - this.buckets[index] = currentNode.next; + this._buckets[index] = currentNode.next; } - this.size--; + this._size--; + currentNode.next = null; // Release memory return; } prevNode = currentNode; currentNode = currentNode.next; } } + + /** + * The `expand` function increases the capacity of a hash table by creating a new array of buckets with double the + * capacity and rehashing all the existing key-value pairs into the new buckets. + */ + protected _expand(): void { + const newCapacity = this._capacity * 2; + const newBuckets = new Array | null>(newCapacity).fill(null); + + for (const bucket of this._buckets) { + let currentNode = bucket; + while (currentNode) { + const newIndex = this._hash(currentNode.key); + const newNode = new HashTableNode(currentNode.key, currentNode.val); + + if (!newBuckets[newIndex]) { + newBuckets[newIndex] = newNode; + } else { + let currentNewNode = newBuckets[newIndex]!; + while (currentNewNode.next) { + currentNewNode = currentNewNode.next; + } + currentNewNode.next = newNode; + } + currentNode = currentNode.next; + } + } + + this._buckets = newBuckets; + this._capacity = newCapacity; + } + + get size(): number { + return this._size; + } } diff --git a/src/data-structures/hash/index.ts b/src/data-structures/hash/index.ts index b4d9dd7..326410c 100644 --- a/src/data-structures/hash/index.ts +++ b/src/data-structures/hash/index.ts @@ -4,3 +4,4 @@ export * from './coordinate-set'; export * from './pair'; export * from './tree-map'; export * from './tree-set'; +export * from './hash-map'; diff --git a/src/data-structures/linked-list/skip-linked-list.ts b/src/data-structures/linked-list/skip-linked-list.ts index a195d75..c1f0ebc 100644 --- a/src/data-structures/linked-list/skip-linked-list.ts +++ b/src/data-structures/linked-list/skip-linked-list.ts @@ -1 +1,166 @@ -export class SkipLinkedList {} +/** + * data-structure-typed + * + * @author Tyler Zeng + * @copyright Copyright (c) 2022 Tyler Zeng + * @license MIT License + */ + +export class SkipListNode { + key: K; + value: V; + forward: SkipListNode[]; + + constructor(key: K, value: V, level: number) { + this.key = key; + this.value = value; + this.forward = new Array(level); + } +} + +export class SkipList { + get probability(): number { + return this._probability; + } + + set probability(value: number) { + this._probability = value; + } + get maxLevel(): number { + return this._maxLevel; + } + + set maxLevel(value: number) { + this._maxLevel = value; + } + get level(): number { + return this._level; + } + + set level(value: number) { + this._level = value; + } + get head(): SkipListNode { + return this._head; + } + + set head(value: SkipListNode) { + this._head = value; + } + private _head: SkipListNode; + private _level: number; + private _maxLevel: number; + private _probability: number; + + /** + * The constructor initializes a SkipList with a specified maximum level and probability. + * @param [maxLevel=16] - The `maxLevel` parameter represents the maximum level that a skip list can have. It determines + * the maximum number of levels that can be created in the skip list. + * @param [probability=0.5] - The probability parameter represents the probability of a node being promoted to a higher + * level in the skip list. It is used to determine the height of each node in the skip list. + */ + constructor(maxLevel = 16, probability = 0.5) { + this._head = new SkipListNode(null as any, null as any, maxLevel); + this._level = 0; + this._maxLevel = maxLevel; + this._probability = probability; + } + + /** + * The function "randomLevel" generates a random level based on a given probability and maximum level. + * @returns the level, which is a number. + */ + private randomLevel(): number { + let level = 1; + while (Math.random() < this.probability && level < this.maxLevel) { + level++; + } + return level; + } + + /** + * The add function adds a new node with a given key and value to a Skip List data structure. + * @param {K} key - The key parameter represents the key of the node that needs to be added to the skip list. + * @param {V} value - The "value" parameter represents the value associated with the key that is being added to the Skip + * List. + */ + add(key: K, value: V): void { + const newNode = new SkipListNode(key, value, this.randomLevel()); + const update: SkipListNode[] = new Array(this.maxLevel).fill(this.head); + let current = this.head; + + for (let i = this.level - 1; i >= 0; i--) { + while (current.forward[i] && current.forward[i].key < key) { + current = current.forward[i]; + } + update[i] = current; + } + + for (let i = 0; i < newNode.forward.length; i++) { + newNode.forward[i] = update[i].forward[i]; + update[i].forward[i] = newNode; + } + + if (newNode.forward[0] !== null) { + this.level = Math.max(this.level, newNode.forward.length); + } + } + + /** + * The function `get` retrieves the value associated with a given key from a skip list data structure. + * @param {K} key - The `key` parameter is the key of the element that we want to retrieve from the data structure. + * @returns The method `get(key: K)` returns the value associated with the given key if it exists in the data structure, + * otherwise it returns `undefined`. + */ + get(key: K): V | undefined { + let current = this.head; + for (let i = this.level - 1; i >= 0; i--) { + while (current.forward[i] && current.forward[i].key < key) { + current = current.forward[i]; + } + } + + current = current.forward[0]; + + if (current && current.key === key) { + return current.value; + } + + return undefined; + } + + /** + * The `remove` function removes a node with a specific key from a Skip List data structure. + * @param {K} key - The key parameter represents the key of the node that needs to be removed from the skip list. + * @returns The `remove` method returns a boolean value. It returns `true` if the key was successfully removed from the + * skip list, and `false` if the key was not found in the skip list. + */ + remove(key: K): boolean { + const update: SkipListNode[] = new Array(this.maxLevel).fill(this.head); + let current = this.head; + + for (let i = this.level - 1; i >= 0; i--) { + while (current.forward[i] && current.forward[i].key < key) { + current = current.forward[i]; + } + update[i] = current; + } + + current = current.forward[0]; + + if (current && current.key === key) { + for (let i = 0; i < this.level; i++) { + if (update[i].forward[i] !== current) { + break; + } + update[i].forward[i] = current.forward[i]; + } + while (this.level > 0 && this.head.forward[this.level - 1] === null) { + this.level--; + } + return true; + } + + return false; + } +} diff --git a/src/types/data-structures/hash.ts b/src/types/data-structures/hash.ts new file mode 100644 index 0000000..4e3977c --- /dev/null +++ b/src/types/data-structures/hash.ts @@ -0,0 +1 @@ +export type HashFunction = (key: K) => number; diff --git a/src/types/data-structures/index.ts b/src/types/data-structures/index.ts index b0b00af..54ef693 100644 --- a/src/types/data-structures/index.ts +++ b/src/types/data-structures/index.ts @@ -13,3 +13,4 @@ export * from './heap'; export * from './singly-linked-list'; export * from './doubly-linked-list'; export * from './navigator'; +export * from './hash'; diff --git a/test/unit/data-structures/binary-tree/segment-tree.test.ts b/test/unit/data-structures/binary-tree/segment-tree.test.ts new file mode 100644 index 0000000..d836045 --- /dev/null +++ b/test/unit/data-structures/binary-tree/segment-tree.test.ts @@ -0,0 +1,50 @@ +import {SegmentTree} from '../../../../src'; + +describe('SegmentTree', () => { + let segmentTree: SegmentTree; + + beforeEach(() => { + // Create an example SegmentTree for testing + const values = [1, 2, 3, 4, 5]; + segmentTree = new SegmentTree(values); + }); + + it('should build a valid segment tree', () => { + // Check if the root node's sum is correct + expect(segmentTree.root?.sum).toBe(15); + }); + + it('should update a node in the segment tree', () => { + // Update a node value + segmentTree.updateNode(2, 10); + + // Check if the sum of the root node is correct after the update + expect(segmentTree.root?.sum).toBe(22); + }); + + it('should query sum by range correctly', () => { + // Check if the sum within a specific range is correct + expect(segmentTree.querySumByRange(1, 3)).toBe(9); // 2 + 3 + 4 = 9 + }); + + it('should handle edge cases for querySumByRange', () => { + // Check behavior when the range goes beyond boundaries + expect(segmentTree.querySumByRange(0, 4)).toBe(15); // Valid range, should return sum of the specified range + expect(segmentTree.querySumByRange(3, 2)).toBe(NaN); // End index is less than start index, should return NaN + expect(segmentTree.querySumByRange(0, 10)).toBe(NaN); // Beyond upper bound, should return NaN + }); + + it('should handle an empty input array', () => { + // Check behavior when dealing with an empty input array + const emptySegmentTree = new SegmentTree([]); + expect(emptySegmentTree.root).toBe(null); + expect(emptySegmentTree.querySumByRange(0, 2)).toBe(0); // Sum of an empty array should be 0 + }); + + it('should handle a single-element input array', () => { + // Check behavior when the input array contains a single element + const singleElementSegmentTree = new SegmentTree([42]); + expect(singleElementSegmentTree.root?.sum).toBe(42); + expect(singleElementSegmentTree.querySumByRange(0, 0)).toBe(42); // Range covering the only element should return that element's value + }); +}); diff --git a/test/unit/data-structures/hash/hash-map.test.ts b/test/unit/data-structures/hash/hash-map.test.ts new file mode 100644 index 0000000..bb18770 --- /dev/null +++ b/test/unit/data-structures/hash/hash-map.test.ts @@ -0,0 +1,104 @@ +import {HashMap} from '../../../../src'; + +describe('HashMap', () => { + let hashMap: HashMap; + + beforeEach(() => { + hashMap = new 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); + }); + + 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.hashFn = () => 0; // Override hash function to return the same index + hashMap.set('key1', 1); + hashMap.set('key2', 2); + + expect(hashMap.get('key1')).toBe(1); + expect(hashMap.get('key2')).toBe(2); + }); + + it('should remove values', () => { + hashMap.set('one', 1); + hashMap.set('two', 2); + + hashMap.remove('one'); + expect(hashMap.get('one')).toBeUndefined(); + expect(hashMap.size).toBe(1); + }); + + it('should clear the HashMap', () => { + 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).toEqual( + expect.arrayContaining([ + ['one', 1], + ['two', 2], + ['three', 3] + ]) + ); + }); + + it('should resize the table when load factor is exceeded', () => { + // Set a small initial capacity for testing resizing + hashMap = new HashMap(4, 0.5); + + 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', () => { + const customHashFn = () => { + // A simple custom hash function that always returns 0 + return 0; + }; + hashMap = new HashMap(16, 0.75, customHashFn); + + 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); + }); +}); diff --git a/test/unit/data-structures/hash/hash-table.test.ts b/test/unit/data-structures/hash/hash-table.test.ts index 275401d..2c252d6 100644 --- a/test/unit/data-structures/hash/hash-table.test.ts +++ b/test/unit/data-structures/hash/hash-table.test.ts @@ -1,10 +1,10 @@ -import {HashNode, HashTable} from '../../../../src'; +import {HashTableNode, HashTable} from '../../../../src'; describe('HashNode', () => { it('should create a HashNode with key and value', () => { const key = 'testKey'; const value = 'testValue'; - const hashNode = new HashNode(key, value); + const hashNode = new HashTableNode(key, value); expect(hashNode.key).toBe(key); expect(hashNode.val).toBe(value); @@ -16,9 +16,9 @@ describe('HashTable', () => { it('should initialize with default capacity', () => { const hashTable = new HashTable(); - expect(hashTable.capacity).toBe(1000); + expect(hashTable.capacity).toBe(16); expect(hashTable.size).toBe(0); - expect(hashTable.buckets.length).toBe(1000); + expect(hashTable.buckets.length).toBe(16); }); it('should initialize with custom capacity', () => { @@ -35,7 +35,7 @@ describe('HashTable', () => { const key = 'testKey'; const value = 'testValue'; - hashTable.put(key, value); + hashTable.set(key, value); const retrievedValue = hashTable.get(key); expect(retrievedValue).toBe(value); @@ -48,8 +48,8 @@ describe('HashTable', () => { const key2 = 'testKey2'; const value2 = 'testValue2'; - hashTable.put(key1, value1); - hashTable.put(key2, value2); + hashTable.set(key1, value1); + hashTable.set(key2, value2); const retrievedValue1 = hashTable.get(key1); const retrievedValue2 = hashTable.get(key2); @@ -64,8 +64,8 @@ describe('HashTable', () => { const initialValue = 'testValue1'; const updatedValue = 'testValue2'; - hashTable.put(key, initialValue); - hashTable.put(key, updatedValue); + hashTable.set(key, initialValue); + hashTable.set(key, updatedValue); const retrievedValue = hashTable.get(key); @@ -86,7 +86,7 @@ describe('HashTable', () => { const key = 'testKey'; const value = 'testValue'; - hashTable.put(key, value); + hashTable.set(key, value); hashTable.remove(key); const retrievedValue = hashTable.get(key); @@ -95,3 +95,90 @@ describe('HashTable', () => { expect(hashTable.size).toBe(0); }); }); + +describe('HashTable', () => { + let hashTable: HashTable; + + beforeEach(() => { + hashTable = new HashTable(); + }); + + it('should insert and retrieve values correctly', () => { + hashTable.set('one', 1); + hashTable.set('two', 2); + + expect(hashTable.get('one')).toBe(1); + expect(hashTable.get('two')).toBe(2); + }); + + it('should update values correctly', () => { + hashTable.set('one', 1); + expect(hashTable.get('one')).toBe(1); + + hashTable.set('one', 100); // Update the value + expect(hashTable.get('one')).toBe(100); + }); + + it('should handle collisions correctly', () => { + hashTable = new HashTable(1); // Set a small capacity to force collisions + hashTable.set('one', 1); + hashTable.set('two', 2); + + expect(hashTable.get('one')).toBe(1); + expect(hashTable.get('two')).toBe(2); + }); + + it('should remove values correctly', () => { + hashTable.set('one', 1); + hashTable.set('two', 2); + hashTable.remove('one'); + + expect(hashTable.get('one')).toBeUndefined(); + expect(hashTable.get('two')).toBe(2); + }); + + it('should handle non-existent keys correctly', () => { + expect(hashTable.get('non-existent')).toBeUndefined(); + hashTable.remove('non-existent'); // Removing a non-existent key should not cause errors + }); + + it('should handle custom hash function correctly', () => { + // const customHashFn = () => { + // // Custom hash function that returns a fixed value for all keys + // return 42; + // }; + + hashTable = new HashTable(16); + hashTable.set('one', 1); + expect(hashTable.get('one')).toBe(1); + expect(hashTable.get('two')).toBeUndefined(); + }); + + it('should expand when load factor exceeds threshold', () => { + hashTable = new HashTable(2); // Set a small capacity to trigger expansion + hashTable.set('one', 1); + hashTable.set('two', 2); + hashTable.set('three', 3); // This should trigger an expansion + + expect(hashTable.capacity).toBe(16); + expect(hashTable.get('one')).toBe(1); + expect(hashTable.get('two')).toBe(2); + expect(hashTable.get('three')).toBe(3); + }); +}); + +describe('HashTable performance', function () { + it('Items set performance', function () { + const mag = 100000; + const ht = new HashTable(); + // const s = performance.now(); + for (let i = 0; i < mag; i++) { + ht.set(i, i); + } + // const s1 = performance.now(); + const map = new Map(); + for (let i = 0; i < mag; i++) { + map.set(i, i); + } + }); +}); diff --git a/test/unit/data-structures/linked-list/skip-list.test.ts b/test/unit/data-structures/linked-list/skip-list.test.ts new file mode 100644 index 0000000..442d6bd --- /dev/null +++ b/test/unit/data-structures/linked-list/skip-list.test.ts @@ -0,0 +1,55 @@ +import {SkipList} from '../../../../src'; + +describe('SkipList', () => { + let skipList: SkipList; + + beforeEach(() => { + skipList = new SkipList(); + }); + + it('should insert and retrieve elements correctly', () => { + skipList.add(1, 'One'); + skipList.add(2, 'Two'); + skipList.add(3, 'Three'); + + expect(skipList.get(1)).toBe('One'); + expect(skipList.get(2)).toBe('Two'); + expect(skipList.get(3)).toBe('Three'); + }); + + it('should return undefined for non-existent keys', () => { + skipList.add(1, 'One'); + skipList.add(2, 'Two'); + + expect(skipList.get(3)).toBeUndefined(); + expect(skipList.get(0)).toBeUndefined(); + }); + + it('should remove elements correctly', () => { + skipList.add(1, 'One'); + skipList.add(2, 'Two'); + skipList.add(3, 'Three'); + + skipList.remove(2); + + expect(skipList.get(2)).toBeUndefined(); // 修改这里的断言 + }); + + it('should handle random data correctly', () => { + const randomData: Array<[number, string]> = [ + [5, 'Five'], + [1, 'One'], + [3, 'Three'], + [2, 'Two'], + [4, 'Four'] + ]; + + for (const [key, value] of randomData) { + skipList.add(key, value); + } + + expect(skipList.get(3)).toBe('Three'); + expect(skipList.get(5)).toBe('Five'); + expect(skipList.get(4)).toBe('Four'); + }); +});