Files
javascript-algorithms/src/data-structures/lru-cache/LRUCache.js
Oleksii Trekhleb 69c3a16f75 Refactor LRU Cache.
2023-01-24 09:04:02 +01:00

154 lines
3.7 KiB
JavaScript

/* eslint-disable no-param-reassign, max-classes-per-file */
/**
* Simple implementation of the Doubly-Linked List Node
* that is used in LRUCache class below.
*/
class LinkedListNode {
/**
* Creates a doubly-linked list node.
* @param {string} key
* @param {any} val
* @param {LinkedListNode} prev
* @param {LinkedListNode} next
*/
constructor(key, val, prev = null, next = null) {
this.key = key;
this.val = val;
this.prev = prev;
this.next = next;
}
}
/**
* Implementation of the LRU (Least Recently Used) Cache
* based on the HashMap and Doubly Linked List data-structures.
*
* Current implementation allows to have fast (O(1)) read and write operations.
*
* At any moment in time the LRU Cache holds not more that "capacity" number of items in it.
*/
class LRUCache {
/**
* Creates a cache instance of a specific capacity.
* @param {number} capacity
*/
constructor(capacity) {
this.capacity = capacity; // How many items to store in cache at max.
this.nodesMap = {}; // The quick links to each linked list node in cache.
this.size = 0; // The number of items that is currently stored in the cache.
this.head = new LinkedListNode(); // The Head (first) linked list node.
this.tail = new LinkedListNode(); // The Tail (last) linked list node.
}
/**
* Returns the cached value by its key.
* Time complexity: O(1).
* @param {string} key
* @returns {any}
*/
get(key) {
if (this.nodesMap[key] === undefined) return undefined;
const node = this.nodesMap[key];
this.promote(node);
return node.val;
}
/**
* Sets the value to cache by its key.
* Time complexity: O(1).
* @param {string} key
* @param {any} val
*/
set(key, val) {
if (this.nodesMap[key]) {
const node = this.nodesMap[key];
node.val = val;
this.promote(node);
} else {
const node = new LinkedListNode(key, val);
this.append(node);
}
}
/**
* Promotes the node to the end of the linked list.
* It means that the node is most frequently used.
* It also reduces the chance for such node to get evicted from cache.
* @param {LinkedListNode} node
*/
promote(node) {
this.evict(node);
this.append(node);
}
/**
* Appends a new node to the end of the cache linked list.
* @param {LinkedListNode} node
*/
append(node) {
this.nodesMap[node.key] = node;
if (!this.head.next) {
// First node to append.
this.head.next = node;
this.tail.prev = node;
node.prev = this.head;
node.next = this.tail;
} else {
// Append to an existing tail.
const oldTail = this.tail.prev;
oldTail.next = node;
node.prev = oldTail;
node.next = this.tail;
this.tail.prev = node;
}
this.size += 1;
if (this.size > this.capacity) {
this.evict(this.head.next);
}
}
/**
* Evicts (removes) the node from cache linked list.
* @param {LinkedListNode} node
*/
evict(node) {
delete this.nodesMap[node.key];
this.size -= 1;
const prevNode = node.prev;
const nextNode = node.next;
// If one and only node.
if (prevNode === this.head && nextNode === this.tail) {
this.head.next = null;
this.tail.prev = null;
this.size = 0;
return;
}
// If this is a Head node.
if (prevNode === this.head) {
nextNode.prev = this.head;
this.head.next = nextNode;
return;
}
// If this is a Tail node.
if (nextNode === this.tail) {
prevNode.next = this.tail;
this.tail.prev = prevNode;
return;
}
// If the node is in the middle.
prevNode.next = nextNode;
nextNode.prev = prevNode;
}
}
export default LRUCache;