[core] HashTable, HashMap, SkipList implemented

This commit is contained in:
Revone 2023-09-29 17:37:30 +08:00
parent 17e0fcb061
commit 1687f3cde8
13 changed files with 906 additions and 100 deletions

View file

@ -9,6 +9,7 @@ on:
paths-ignore:
- 'lib/**'
- 'dist/**'
- 'umd/**'
- 'docs/**'
- 'coverage/**'
- '**/*.md'

View file

@ -441,24 +441,17 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D
<tr>
<td>Set</td>
<td>std::set&lt;T&gt;</td>
<td>Set</td>
<td>Set&lt;E&gt</td>
<td>HashSet&lt;E&gt;</td>
<td>set</td>
</tr>
<tr>
<td>Map</td>
<td>std::map&lt;K, V&gt;</td>
<td>Map</td>
<td>Map&lt;K, V&gt;</td>
<td>HashMap&lt;K, V&gt;</td>
<td>dict</td>
</tr>
<tr>
<td>Unordered Map</td>
<td>std::unordered_map&lt;K, V&gt;</td>
<td>N/A</td>
<td>HashMap&lt;K, V&gt;</td>
<td>defaultdict</td>
</tr>
<tr>
<td>Unordered Set</td>
<td>std::unordered_set&lt;T&gt;</td>
@ -466,24 +459,31 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D
<td>HashSet&lt;E&gt;</td>
<td>N/A</td>
</tr>
<tr>
<td>Unordered Map</td>
<td>std::unordered_map&lt;K, V&gt;</td>
<td>HashTable&lt;K, V&gt;</td>
<td>HashMap&lt;K, V&gt;</td>
<td>defaultdict</td>
</tr>
<tr>
<td>Queue</td>
<td>std::queue&lt;T&gt;</td>
<td>Queue</td>
<td>Queue&lt;E&gt;</td>
<td>Queue&lt;E&gt;</td>
<td>N/A</td>
</tr>
<tr>
<td>Priority Queue</td>
<td>std::priority_queue&lt;T&gt;</td>
<td>PriorityQueue</td>
<td>PriorityQueue&lt;E&gt;</td>
<td>PriorityQueue&lt;E&gt;</td>
<td>N/A</td>
</tr>
<tr>
<td>Stack</td>
<td>std::stack&lt;T&gt;</td>
<td>Stack</td>
<td>Stack&lt;E&gt;</td>
<td>Stack&lt;E&gt;</td>
<td>N/A</td>
</tr>
@ -497,7 +497,7 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D
<tr>
<td>Deque</td>
<td>std::deque&lt;T&gt;</td>
<td>Deque</td>
<td>Deque&lt;E&gt;</td>
<td>N/A</td>
<td>N/A</td>
</tr>
@ -525,14 +525,14 @@ Array.from(dijkstraResult?.seen ?? []).map(vertex => vertex.id) // ['A', 'B', 'D
<tr>
<td>Ordered Dictionary</td>
<td>N/A</td>
<td>Map</td>
<td>Map&lt;K, V&gt;</td>
<td>N/A</td>
<td>OrderedDict</td>
</tr>
<tr>
<td>Double-Ended Queue (Deque)</td>
<td>std::deque&lt;T&gt;</td>
<td>Deque</td>
<td>Deque&lt;E&gt;</td>
<td>N/A</td>
<td>N/A</td>
</tr>

View file

@ -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);

View file

@ -0,0 +1,203 @@
import {HashFunction} from '../../types';
/**
* data-structure-typed
*
* @author Tyler Zeng
* @copyright Copyright (c) 2022 Tyler Zeng <zrwusa@gmail.com>
* @license MIT License
*/
export class HashMap<K, V> {
get hashFn(): HashFunction<K> {
return this._hashFn;
}
set hashFn(value: HashFunction<K>) {
this._hashFn = value;
}
get table(): Array<Array<[K, V]>> {
return this._table;
}
set table(value: Array<Array<[K, V]>>) {
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<Array<[K, V]>>;
private _hashFn: HashFunction<K>;
/**
* 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<K>) {
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;
}
}

View file

@ -5,10 +5,11 @@
* @copyright Copyright (c) 2022 Tyler Zeng <zrwusa@gmail.com>
* @license MIT License
*/
export class HashNode<K, V> {
export class HashTableNode<K, V> {
key: K;
val: V;
next: HashNode<K, V> | null;
next: HashTableNode<K, V> | null;
constructor(key: K, val: V) {
this.key = key;
@ -17,23 +18,25 @@ export class HashNode<K, V> {
}
}
import {HashFunction} from '../../types';
export class HashTable<K, V> {
get buckets(): Array<HashNode<K, V> | null> {
get hashFn(): HashFunction<K> {
return this._hashFn;
}
set hashFn(value: HashFunction<K>) {
this._hashFn = value;
}
get buckets(): Array<HashTableNode<K, V> | null> {
return this._buckets;
}
set buckets(value: Array<HashNode<K, V> | null>) {
set buckets(value: Array<HashTableNode<K, V> | 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<K, V> {
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<HashNode<K, V> | null>;
private _buckets: Array<HashTableNode<K, V> | null>;
private _hashFn: HashFunction<K>;
/**
* 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<K>) {
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<HashTableNode<K, V> | 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<K>(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<K>(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<K, V>(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<K, V> {
}
/**
* 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<K, V> | null = null;
const index = this._hash(key);
let currentNode = this._buckets[index];
let prevNode: HashTableNode<K, V> | 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<HashTableNode<K, V> | 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<K, V>(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;
}
}

View file

@ -4,3 +4,4 @@ export * from './coordinate-set';
export * from './pair';
export * from './tree-map';
export * from './tree-set';
export * from './hash-map';

View file

@ -1 +1,166 @@
export class SkipLinkedList {}
/**
* data-structure-typed
*
* @author Tyler Zeng
* @copyright Copyright (c) 2022 Tyler Zeng <zrwusa@gmail.com>
* @license MIT License
*/
export class SkipListNode<K, V> {
key: K;
value: V;
forward: SkipListNode<K, V>[];
constructor(key: K, value: V, level: number) {
this.key = key;
this.value = value;
this.forward = new Array(level);
}
}
export class SkipList<K, V> {
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<K, V> {
return this._head;
}
set head(value: SkipListNode<K, V>) {
this._head = value;
}
private _head: SkipListNode<K, V>;
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<K, V>(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<K, V>[] = 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<K, V>[] = 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;
}
}

View file

@ -0,0 +1 @@
export type HashFunction<K> = (key: K) => number;

View file

@ -13,3 +13,4 @@ export * from './heap';
export * from './singly-linked-list';
export * from './doubly-linked-list';
export * from './navigator';
export * from './hash';

View file

@ -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
});
});

View file

@ -0,0 +1,104 @@
import {HashMap} from '../../../../src';
describe('HashMap', () => {
let hashMap: HashMap<string, number>;
beforeEach(() => {
hashMap = new HashMap<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.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<string, number>(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<string, number>(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);
});
});

View file

@ -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<string, string>();
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<string, number>;
beforeEach(() => {
hashTable = new HashTable<string, number>();
});
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<string, number>(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<string, number>(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<string, number>(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);
}
});
});

View file

@ -0,0 +1,55 @@
import {SkipList} from '../../../../src';
describe('SkipList', () => {
let skipList: SkipList<number, string>;
beforeEach(() => {
skipList = new SkipList<number, string>();
});
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');
});
});