279 lines
7.1 KiB
TypeScript
279 lines
7.1 KiB
TypeScript
import type { IHashMap } from "../interfaces/IHashMap.ts";
|
|
import type { IHashFunction } from "../interfaces/IHashFunction.ts";
|
|
import { HashNode } from "../models/HashNode.ts";
|
|
import { DefaultHashFunction } from "../hash-functions/DefaultHashFunction.ts";
|
|
|
|
/**
|
|
* HashMap implementation using separate chaining for collision resolution.
|
|
* Follows SOLID principles:
|
|
* - SRP: Focused on hash map operations
|
|
* - OCP: Extensible through custom hash functions
|
|
* - LSP: Implements IHashMap interface correctly
|
|
* - ISP: Uses focused interfaces
|
|
* - DIP: Depends on IHashFunction abstraction
|
|
*
|
|
* @template K - The type of keys
|
|
* @template V - The type of values
|
|
*/
|
|
export class HashMap<K, V> implements IHashMap<K, V>, Iterable<[K, V]> {
|
|
private buckets: (HashNode<K, V> | null)[];
|
|
private _size: number = 0;
|
|
private readonly hashFunction: IHashFunction<K>;
|
|
private readonly loadFactorThreshold: number;
|
|
private readonly initialCapacity: number;
|
|
|
|
/**
|
|
* Creates a new HashMap instance.
|
|
* @param initialCapacity - Initial number of buckets (default: 16)
|
|
* @param loadFactorThreshold - Threshold for resizing (default: 0.75)
|
|
* @param hashFunction - Custom hash function (optional)
|
|
*/
|
|
constructor(
|
|
initialCapacity: number = 16,
|
|
loadFactorThreshold: number = 0.75,
|
|
hashFunction?: IHashFunction<K>,
|
|
) {
|
|
if (initialCapacity <= 0) {
|
|
throw new Error("Initial capacity must be positive");
|
|
}
|
|
if (loadFactorThreshold <= 0 || loadFactorThreshold > 1) {
|
|
throw new Error("Load factor must be between 0 and 1");
|
|
}
|
|
|
|
this.initialCapacity = initialCapacity;
|
|
this.buckets = new Array(initialCapacity).fill(null);
|
|
this.loadFactorThreshold = loadFactorThreshold;
|
|
this.hashFunction = hashFunction ?? new DefaultHashFunction<K>();
|
|
}
|
|
|
|
/**
|
|
* Gets the current number of key-value pairs in the map.
|
|
*/
|
|
get size(): number {
|
|
return this._size;
|
|
}
|
|
|
|
/**
|
|
* Gets the current capacity (number of buckets).
|
|
*/
|
|
get capacity(): number {
|
|
return this.buckets.length;
|
|
}
|
|
|
|
/**
|
|
* Gets the current load factor.
|
|
*/
|
|
get loadFactor(): number {
|
|
return this._size / this.buckets.length;
|
|
}
|
|
|
|
/**
|
|
* Inserts or updates a key-value pair in the map.
|
|
* Time Complexity: Average O(1), Worst O(n)
|
|
*/
|
|
set(key: K, value: V): void {
|
|
this.ensureCapacity();
|
|
|
|
const index = this.hashFunction.hash(key, this.buckets.length);
|
|
let node = this.buckets[index] ?? null;
|
|
|
|
// Check if key already exists
|
|
while (node !== null) {
|
|
if (this.keysEqual(node.key, key)) {
|
|
node.value = value; // Update existing value
|
|
return;
|
|
}
|
|
node = node.next;
|
|
}
|
|
|
|
// Insert new node at the beginning of the chain
|
|
const newNode = new HashNode(key, value, this.buckets[index] ?? null);
|
|
this.buckets[index] = newNode;
|
|
this._size++;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the value associated with the given key.
|
|
* Time Complexity: Average O(1), Worst O(n)
|
|
*/
|
|
get(key: K): V | undefined {
|
|
const index = this.hashFunction.hash(key, this.buckets.length);
|
|
let node = this.buckets[index] ?? null;
|
|
|
|
while (node !== null) {
|
|
if (this.keysEqual(node.key, key)) {
|
|
return node.value;
|
|
}
|
|
node = node.next;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Checks if a key exists in the map.
|
|
* Time Complexity: Average O(1), Worst O(n)
|
|
*/
|
|
has(key: K): boolean {
|
|
const index = this.hashFunction.hash(key, this.buckets.length);
|
|
let node = this.buckets[index] ?? null;
|
|
|
|
while (node !== null) {
|
|
if (this.keysEqual(node.key, key)) {
|
|
return true;
|
|
}
|
|
node = node.next;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes a key-value pair from the map.
|
|
* Time Complexity: Average O(1), Worst O(n)
|
|
*/
|
|
delete(key: K): boolean {
|
|
const index = this.hashFunction.hash(key, this.buckets.length);
|
|
let node = this.buckets[index] ?? null;
|
|
let prev: HashNode<K, V> | null = null;
|
|
|
|
while (node !== null) {
|
|
if (this.keysEqual(node.key, key)) {
|
|
if (prev === null) {
|
|
// Remove head node
|
|
this.buckets[index] = node.next;
|
|
} else {
|
|
// Remove middle or tail node
|
|
prev.next = node.next;
|
|
}
|
|
this._size--;
|
|
return true;
|
|
}
|
|
prev = node;
|
|
node = node.next;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes all key-value pairs from the map.
|
|
* Time Complexity: O(n) where n is the capacity
|
|
*/
|
|
clear(): void {
|
|
this.buckets = new Array(this.initialCapacity).fill(null);
|
|
this._size = 0;
|
|
}
|
|
|
|
/**
|
|
* Returns an iterator over the keys in the map.
|
|
*/
|
|
*keys(): IterableIterator<K> {
|
|
for (const bucket of this.buckets) {
|
|
let node = bucket ?? null;
|
|
while (node !== null) {
|
|
yield node.key;
|
|
node = node.next;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an iterator over the values in the map.
|
|
*/
|
|
*values(): IterableIterator<V> {
|
|
for (const bucket of this.buckets) {
|
|
let node = bucket ?? null;
|
|
while (node !== null) {
|
|
yield node.value;
|
|
node = node.next;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an iterator over the key-value pairs in the map.
|
|
*/
|
|
*entries(): IterableIterator<[K, V]> {
|
|
for (const bucket of this.buckets) {
|
|
let node = bucket ?? null;
|
|
while (node !== null) {
|
|
yield [node.key, node.value];
|
|
node = node.next;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes the HashMap iterable (for...of loops).
|
|
*/
|
|
[Symbol.iterator](): IterableIterator<[K, V]> {
|
|
return this.entries();
|
|
}
|
|
|
|
/**
|
|
* Executes a callback for each key-value pair in the map.
|
|
*/
|
|
forEach(callback: (value: V, key: K, map: IHashMap<K, V>) => void): void {
|
|
for (const [key, value] of this.entries()) {
|
|
callback(value, key, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of the map.
|
|
*/
|
|
toString(): string {
|
|
const entries: string[] = [];
|
|
for (const [key, value] of this.entries()) {
|
|
entries.push(`${String(key)} => ${String(value)}`);
|
|
}
|
|
return `HashMap(${this._size}) { ${entries.join(", ")} }`;
|
|
}
|
|
|
|
/**
|
|
* Checks if two keys are equal.
|
|
* Handles primitive types and object references.
|
|
*/
|
|
private keysEqual(key1: K, key2: K): boolean {
|
|
// Handle NaN case
|
|
if (typeof key1 === "number" && typeof key2 === "number") {
|
|
if (Number.isNaN(key1) && Number.isNaN(key2)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Standard equality
|
|
return key1 === key2;
|
|
}
|
|
|
|
/**
|
|
* Ensures the map has sufficient capacity.
|
|
* Resizes if load factor exceeds threshold.
|
|
*/
|
|
private ensureCapacity(): void {
|
|
if (this.loadFactor >= this.loadFactorThreshold) {
|
|
this.resize(this.buckets.length * 2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resizes the hash table to the new capacity.
|
|
* Rehashes all existing entries.
|
|
*/
|
|
private resize(newCapacity: number): void {
|
|
const oldBuckets = this.buckets;
|
|
this.buckets = new Array(newCapacity).fill(null);
|
|
this._size = 0;
|
|
|
|
// Rehash all existing entries
|
|
for (const bucket of oldBuckets) {
|
|
let node = bucket ?? null;
|
|
while (node !== null) {
|
|
this.set(node.key, node.value);
|
|
node = node.next;
|
|
}
|
|
}
|
|
}
|
|
}
|