diff --git a/data_structures/cache/BaseCache.ts b/data_structures/cache/BaseCache.ts new file mode 100644 index 00000000..dc59874c --- /dev/null +++ b/data_structures/cache/BaseCache.ts @@ -0,0 +1,45 @@ +/** + * Represents the foundational architecture for caching mechanisms. + * + * This abstract class defines the standard contract and shared state + * required by specialized cache implementations, ensuring a consistent + * interface for data storage and retrieval. + * + * @template K - The type of keys maintained by the cache. + * @template V - The type of mapped values. + */ +export abstract class BaseCache { + /** + * The maximum number of items the cache is permitted to hold + * before eviction policies are triggered. + */ + protected capacity: number; + + /** + * Initializes a new instance of the BaseCache. + * + * @param capacity - The maximum allowed capacity of the cache. + * @throws {Error} If the provided capacity is less than or equal to zero. + */ + constructor(capacity: number) { + if (capacity <= 0) throw new Error("Capacity must be positive"); + this.capacity = capacity; + } + + /** + * Retrieves the value associated with the specified key. + * + * @param key - The key whose associated value is to be returned. + * @returns The value associated with the key, or undefined if the key does not exist. + */ + abstract get(key: K): V | undefined; + + /** + * Associates the specified value with the specified key in the cache. + * If the cache previously contained a mapping for the key, the old value is replaced. + * + * @param key - The key with which the specified value is to be associated. + * @param value - The value to be associated with the specified key. + */ + abstract put(key: K, value: V): void; +} \ No newline at end of file diff --git a/data_structures/cache/LRUCache.ts b/data_structures/cache/LRUCache.ts new file mode 100644 index 00000000..9b04359f --- /dev/null +++ b/data_structures/cache/LRUCache.ts @@ -0,0 +1,164 @@ +import { BaseCache } from './BaseCache'; + +/** + * Represents a single node within the doubly linked list utilized by the LRU Cache. + * + * @template K - The type of the key. + * @template V - The type of the value. + */ +class Node { + /** + * Initializes a new instance of the Node. + * + * @param key - The identifying key for the node. + * @param value - The data value stored within the node. + * @param prev - Reference to the preceding node in the sequence. + * @param next - Reference to the succeeding node in the sequence. + */ + constructor( + public key: K, + public value: V, + public prev: Node | null = null, + public next: Node | null = null + ) { } +} + +/** + * A Least Recently Used (LRU) Cache implementation. + * + * This advanced cache automatically evicts the least recently accessed items + * when the strictly defined capacity limit is reached. It systematically combines + * a native Hash Map for constant-time lookups with a Doubly Linked List to + * guarantee that both `get` and `put` operations execute in rigorous O(1) time complexity. + * + * @template K - The type of keys maintained by the cache. + * @template V - The type of mapped values. + */ +export class LRUCache extends BaseCache { + /** + * An internal hash map establishing an O(1) lookup index for cache nodes. + */ + private map: Map> = new Map(); + + /** + * The vanguard of the doubly linked list, exclusively holding the most recently utilized item. + */ + private head: Node | null = null; + + /** + * The rearguard of the doubly linked list, designating the least recently utilized item + * (the prime candidate for the next eviction). + */ + private tail: Node | null = null; + + /** + * Instantiates an LRUCache with a precise maximum capacity. + * + * @param capacity - The absolute maximum number of key-value pairs the cache can concurrently retain. + */ + constructor(capacity: number) { + super(capacity); + } + + /** + * Retrieves the precise value associated with the specified key, synchronously + * elevating the corresponding item to "most recently used" status. + * + * @param key - The key whose associated value is to be targeted. + * @returns The value associated with the key, or `undefined` if the cache miss occurs. + */ + get(key: K): V | undefined { + const node = this.map.get(key); + if (!node) return undefined; + + this.moveToFront(node); + return node.value; + } + + /** + * Inserts or seamlessly updates a key-value mapping within the cache structure. + * If the addition violates the cache's capacity constraint, the least recently + * utilized item is systematically purged. + * + * @param key - The key to insert or update. + * @param value - The updated value to bind to the given key. + */ + put(key: K, value: V): void { + const existingNode = this.map.get(key); + + if (existingNode) { + existingNode.value = value; + this.moveToFront(existingNode); + } else { + if (this.map.size >= this.capacity) { + this.evictLeastRecentlyUsed(); + } + + const newNode = new Node(key, value); + this.map.set(key, newNode); + this.addToFront(newNode); + } + } + + /** + * Re-links a specific, active node to the pinnacle (head) of the linked list. + * + * @param node - The node requiring promotion. + */ + private moveToFront(node: Node): void { + if (node === this.head) return; + + this.removeNode(node); + this.addToFront(node); + } + + /** + * Injects a novel node directly at the head of the linked list. + * + * @param node - The freshly instantiated node to insert. + */ + private addToFront(node: Node): void { + node.next = this.head; + node.prev = null; + + if (this.head) { + this.head.prev = node; + } + + this.head = node; + + if (!this.tail) { + this.tail = node; + } + } + + /** + * Surgically detaches a specific node from its surrounding linked list neighbors. + * + * @param node - The target node to isolate and remove. + */ + private removeNode(node: Node): void { + if (node.prev) { + node.prev.next = node.next; + } else { + this.head = node.next; + } + + if (node.next) { + node.next.prev = node.prev; + } else { + this.tail = node.prev; + } + } + + /** + * Executes the eviction protocol, purging the least recently utilized node + * from both the linked list hierarchy and the primary map index. + */ + private evictLeastRecentlyUsed(): void { + if (!this.tail) return; + + this.map.delete(this.tail.key); + this.removeNode(this.tail); + } +} \ No newline at end of file diff --git a/data_structures/cache/test/LRUCache.test.ts b/data_structures/cache/test/LRUCache.test.ts new file mode 100644 index 00000000..438e7e75 --- /dev/null +++ b/data_structures/cache/test/LRUCache.test.ts @@ -0,0 +1,79 @@ +import { LRUCache } from '../LRUCache'; +import { BaseCache } from '../BaseCache'; + +/** + * Test Suite: Least Recently Used (LRU) Cache Implementation + * + * This suite rigorously validates the functional requirements of the LRUCache, + * ensuring robust key-value storage, correct eviction policies upon reaching + * capacity, and O(1) update characteristics when items are accessed. + */ +describe('LRUCache', () => { + let lru: LRUCache; + + beforeEach(() => { + // Initialize the cache with a constrained capacity to systematically trigger and verify eviction logic. + lru = new LRUCache(3); + }); + + it('should be an instance of BaseCache', () => { + expect(lru).toBeInstanceOf(BaseCache); + }); + + it('should successfully store and retrieve standard key-value pairs', () => { + lru.put('a', 1); + lru.put('b', 2); + expect(lru.get('a')).toBe(1); + expect(lru.get('b')).toBe(2); + }); + + it('should gracefully return undefined for queries on non-existent keys', () => { + expect(lru.get('non-existent')).toBeUndefined(); + }); + + it('should accurately update the value corresponding to an existing key', () => { + lru.put('a', 1); + lru.put('a', 10); + expect(lru.get('a')).toBe(10); + }); + + it('should strictly evict the least recently used item when the maximum capacity is exceeded', () => { + lru.put('a', 1); // State: [a] + lru.put('b', 2); // State: [b, a] + lru.put('c', 3); // State: [c, b, a] + + // Inserting a 4th element must trigger the eviction of 'a' (the current tail) + lru.put('d', 4); // State: [d, c, b] + + expect(lru.get('a')).toBeUndefined(); + expect(lru.get('b')).toBe(2); + expect(lru.get('c')).toBe(3); + expect(lru.get('d')).toBe(4); + }); + + it('should successfully re-prioritize an accessed item to the front of the cache (Most Recently Used)', () => { + lru.put('a', 1); + lru.put('b', 2); + lru.put('c', 3); // State: [c, b, a] + + // Accessing 'a' promotes it to the head, making it the most recently used + lru.get('a'); // State: [a, c, b] + + // Consequently, 'b' falls to the tail position and becomes the next candidate for eviction + lru.put('d', 4); // State: [d, a, c] + + expect(lru.get('b')).toBeUndefined(); + expect(lru.get('a')).toBe(1); + expect(lru.get('c')).toBe(3); + expect(lru.get('d')).toBe(4); + }); + + it('should maintain strict operational integrity even with a minimal capacity of 1', () => { + const smallLru = new LRUCache(1); + smallLru.put('a', 1); + smallLru.put('b', 2); + + expect(smallLru.get('a')).toBeUndefined(); + expect(smallLru.get('b')).toBe(2); + }); +}); \ No newline at end of file