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 implements IHashMap, Iterable<[K, V]> { private buckets: (HashNode | null)[]; private _size: number = 0; private readonly hashFunction: IHashFunction; 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, ) { 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(); } /** * 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 | 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 { 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 { 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) => 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; } } } }