diff --git a/public/algo_buttons/LRU Cache.png b/public/algo_buttons/LRU Cache.png new file mode 100644 index 0000000..d28c970 Binary files /dev/null and b/public/algo_buttons/LRU Cache.png differ diff --git a/src/AlgoList.js b/src/AlgoList.js index 2704d1b..fb9ad9d 100644 --- a/src/AlgoList.js +++ b/src/AlgoList.js @@ -56,6 +56,7 @@ export const algoMap = { Kruskal: ["Kruskal's", algos.Kruskals, true], DisjointSet: ['Disjoint Set', algos.DisjointSet, true], LCS: ['LCS', algos.LCS, true, 'Longest Common Subsequence'], + LRU: ['LRU Cache', algos.LRU, true, 'LRU Cache'], Floyd: ['Floyd-Warshall', algos.Floyd], TreeMap: ['TreeMap', algos.TreeMap], }; @@ -120,6 +121,7 @@ const baseAlgoList = [ '---', 'DP & Extras', 'SplayTree', + 'LRU', 'Floyd', 'DropSort', 'SleepSort', @@ -197,7 +199,8 @@ export const relatedSearches = { Dijkstra: ['BFS', 'DFS', 'Prim', 'Kruskal', 'DisjointSet'], Prim: ['BFS', 'DFS', 'Dijkstra', 'Kruskal', 'DisjointSet'], Kruskal: ['BFS', 'DFS', 'Dijkstra', 'Prim', 'DisjointSet'], - LCS: ['Floyd'], + LCS: ['Floyd', 'LRU'], + LRU: ['ClosedHash', 'OpenHash', 'DoublyLinkedList', 'LCS'], Floyd: ['LCS'], TreeMap: ['AVL', 'ClosedHash', 'OpenHash'], }; @@ -351,6 +354,10 @@ export const algoFilter = [ id: 'Floyd', category: 'DP & Extras', }, + { + id: 'LRU', + category: 'DP & Extras', + }, { id: 'CreateGraph', category: 'Graphs', diff --git a/src/algo/LRU.js b/src/algo/LRU.js new file mode 100644 index 0000000..167cdde --- /dev/null +++ b/src/algo/LRU.js @@ -0,0 +1,894 @@ +// Copyright 2011 David Galles, University of San Francisco. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Algorithm, { + addControlToAlgorithmBar, + addDivisorToAlgorithmBar, + addGroupToAlgorithmBar, + addLabelToAlgorithmBar, +} from './Algorithm.js'; +import { act } from '../anim/AnimationMain'; +import pseudocodeText from '../pseudocode.json'; + +// Layout Configurations + +const INFO_MSG_X = 25; +const INFO_MSG_Y = 15; + +const CAP_LABEL_X = 25; +const CAP_LABEL_Y = 40; + +// Cache Table (Left side) +const MAP_TITLE_X = 25; +const MAP_TITLE_Y = 75; +const MAP_COL_KEY_X = 48; +const MAP_COL_REF_X = 98; +const MAP_COL_HEADER_Y = 100; +const MAP_START_Y = 130; +const MAP_ROW_GAP = 32; +const MAP_KEY_W = 46; +const MAP_REF_W = 34; +const MAP_ROW_H = 24; + +// Doubly Linked List (Right side) +const DLL_START_X = 220; +const DLL_START_Y = 200; +const DLL_NODE_W = 65; +const DLL_NODE_H = 30; +const DLL_X_GAP = 95; + +// Value label sits below each DLL node +const VAL_OFFSET_Y = DLL_NODE_H / 2 + 15; + +// MRU / LRU badges sit with comfortable clearance above the DLL row +const BADGE_W = 45; +const BADGE_H = 22; +const BADGE_Y = DLL_START_Y - 50; + +// Off-screen position used to "hide" canvas objects we can't destroy +const OFFSCREEN = -2000; + +// Colors + +const MRU_COLOR = '#006400'; // dark green +const LRU_COLOR = '#CC0000'; // crimson red +const COMBO_BADGE_COLOR = '#4A148C'; // dark purple for capacity/size 1 MRU/LRU badge +const HIGHLIGHT_COLOR = '#FFD700'; // gold — insertion flash / update +const EVICT_COLOR = '#FF6666'; // red — eviction / deletion +const FOUND_COLOR = '#90EE90'; // light green — get hit +const MAP_LINK_COLOR = '#1976D2'; // blue for active Hash Map -> DLL node references +const KEY_CELL_BG = '#F8F9FA'; +const PTR_CELL_BG = '#E3F2FD'; +const CELL_BORDER = '#B0BEC5'; +const WHITE = '#FFFFFF'; + +// Limits for the simulation + +const MAX_CAPACITY = 10; +const DEFAULT_CAPACITY = 5; +const MAX_KEY_LENGTH = 4; +const MAX_VAL_LENGTH = 4; + +// Keys that would cause prototype pollution +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + +// Status Message Helpers + +export const STATUS = { + // Validation / Input errors + NEED_KEY_VALUE: 'Please enter both a key and a value.', + FORBIDDEN_KEY: key => `Key "${key}" is not allowed.`, + NEED_GET_KEY: 'Please enter a key to get.', + NEED_DEL_KEY: 'Please enter a key to delete.', + INVALID_CAPACITY: max => `Capacity must be an integer between 1 and ${max}.`, + + // Put + PUT_HIT: (key, val) => `Cache Hit on key "${key}" — updating value to "${val}".`, + PUT_EVICT: (cap, key) => `Cache full (${cap}/${cap}) — evicting LRU key "${key}".`, + PUT_INSERT: (key, val) => `Inserting "${key}" → "${val}" into Cache and DLL at MRU position.`, + PUT_COMPLETE: (key, val) => `put("${key}", "${val}") complete.`, + + // Get + GET_MISS: key => `Cache Miss: key "${key}" not found in Cache — returning -1.`, + GET_HIT: key => `Cache Hit! Key "${key}" located in Cache (O(1)) → pointing to DLL node.`, + GET_COMPLETE: (key, val) => `get("${key}") → "${val}".`, + + // Delete + DEL_MISS: key => `Key "${key}" not in Cache — nothing to delete.`, + DEL_FOUND: key => `Deleting key "${key}" from Cache and DLL.`, + DEL_COMPLETE: key => `delete("${key}") complete.`, + + // Navigation & Inspection + MOVE_TO_MRU: key => `Moving "${key}" to MRU position.`, + INSPECT: (key, val, pos) => + `Inspecting: key "${key}" maps to DLL node "${key}" (value: "${val}") at ${pos}.`, +}; + +// LRU Visualizer + +export default class LRU extends Algorithm { + constructor(am, w, h) { + super(am, w, h); + this.addControls(); + this.nextIndex = 0; + this.commands = []; + this.inspectedKey = null; + this.listenersAttached = false; + this.setup(); + } + + // Control bar + + addInputField(parentGroup, size, callback, isIntOnly = false, initialVal = '') { + const field = addControlToAlgorithmBar('Text', initialVal, parentGroup); + field.size = size; + field.onkeydown = this.returnSubmit(field, callback.bind(this), size, isIntOnly); + this.controls.push(field); + return field; + } + + addButton(label, callback, parentGroup) { + const button = addControlToAlgorithmBar('Button', label, parentGroup); + button.onclick = callback.bind(this); + this.controls.push(button); + return button; + } + + addControls() { + this.controls = []; + + // Put: Key / Value (two-row group) + const putVG = addGroupToAlgorithmBar(false); + const putTop = addGroupToAlgorithmBar(true, putVG); + const putBot = addGroupToAlgorithmBar(true, putVG); + + addLabelToAlgorithmBar('Key:', putTop); + this.putKeyField = this.addInputField(putTop, MAX_KEY_LENGTH, this.putCallback); + + addLabelToAlgorithmBar('Value:', putBot); + this.putValField = this.addInputField(putBot, MAX_VAL_LENGTH, this.putCallback); + + this.putButton = this.addButton('Put', this.putCallback); + + addDivisorToAlgorithmBar(); + + // Get: Key + const getG = addGroupToAlgorithmBar(true); + addLabelToAlgorithmBar('Key:', getG); + this.getKeyField = this.addInputField(getG, MAX_KEY_LENGTH, this.getCallback); + this.getButton = this.addButton('Get', this.getCallback); + + addDivisorToAlgorithmBar(); + + // Delete: Key + const delG = addGroupToAlgorithmBar(true); + addLabelToAlgorithmBar('Key:', delG); + this.deleteKeyField = this.addInputField(delG, MAX_KEY_LENGTH, this.deleteCallback); + this.deleteButton = this.addButton('Delete', this.deleteCallback); + + addDivisorToAlgorithmBar(); + + // Capacity + Restart + const capVG = addGroupToAlgorithmBar(false); + const capTop = addGroupToAlgorithmBar(true, capVG); + const capBot = addGroupToAlgorithmBar(true, capVG); + + addLabelToAlgorithmBar('Capacity:', capTop); + this.capacityField = this.addInputField( + capTop, + 3, + this.restartCallback, + true, + String(DEFAULT_CAPACITY), + ); + this.restartButton = this.addButton('Restart', this.restartCallback, capBot); + + addDivisorToAlgorithmBar(); + + // Clear button + this.clearButton = this.addButton('Clear', this.clearCallback); + } + + setup() { + this.capacity = DEFAULT_CAPACITY; + this.map = Object.create(null); + this.mapKeys = []; + this.dll = []; + this.dllConnections = []; + + // Canvas Headers + + this.infoLabelID = this.nextIndex++; + this.cmd(act.createLabel, this.infoLabelID, '', INFO_MSG_X, INFO_MSG_Y, 0); + + // Capacity counter text + this.capLabelID = this.nextIndex++; + this.cmd(act.createLabel, this.capLabelID, this.capText(), CAP_LABEL_X, CAP_LABEL_Y, 0); + + // Cache section title & column headers + this.mapTitleID = this.nextIndex++; + this.cmd(act.createLabel, this.mapTitleID, 'Cache', MAP_TITLE_X, MAP_TITLE_Y, 0); + this.cmd(act.setForegroundColor, this.mapTitleID, '#1976D2'); + + this.mapColKeyID = this.nextIndex++; + this.cmd(act.createLabel, this.mapColKeyID, 'Key', MAP_COL_KEY_X, MAP_COL_HEADER_Y, 1); + this.cmd(act.setForegroundColor, this.mapColKeyID, '#666666'); + + this.mapColRefID = this.nextIndex++; + this.cmd(act.createLabel, this.mapColRefID, 'Node Ref', MAP_COL_REF_X, MAP_COL_HEADER_Y, 1); + this.cmd(act.setForegroundColor, this.mapColRefID, '#666666'); + + // "val:" label sits to the left of the value row + this.valHeaderID = this.nextIndex++; + this.cmd( + act.createLabel, + this.valHeaderID, + 'val:', + DLL_START_X - 45, + DLL_START_Y + VAL_OFFSET_Y, + 0, + ); + this.cmd(act.setForegroundColor, this.valHeaderID, '#888888'); + + // MRU badge — colored rectangle + text + this.mruBadgeID = this.nextIndex++; + this.mruTextID = this.nextIndex++; + this.cmd(act.createRectangle, this.mruBadgeID, '', BADGE_W, BADGE_H, OFFSCREEN, OFFSCREEN); + this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.createLabel, this.mruTextID, '', OFFSCREEN, OFFSCREEN, 1); + this.cmd(act.setForegroundColor, this.mruTextID, WHITE); + + // LRU badge — colored rectangle + text + this.lruBadgeID = this.nextIndex++; + this.lruTextID = this.nextIndex++; + this.cmd(act.createRectangle, this.lruBadgeID, '', BADGE_W, BADGE_H, OFFSCREEN, OFFSCREEN); + this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.createLabel, this.lruTextID, '', OFFSCREEN, OFFSCREEN, 1); + this.cmd(act.setForegroundColor, this.lruTextID, WHITE); + + this.pseudocode = pseudocodeText.LRU; + + // Store nextIndex after setup so reset() can restore it + this.resetIndex = this.nextIndex; + + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.animationManager.clearHistory(); + this.commands = []; + + // Attach canvas event listeners for interactive Click-to-Inspect + this.canvas = document.getElementById('canvas'); + if (this.canvas && !this.listenersAttached) { + this.canvas.addEventListener('click', this.handleCanvasClick.bind(this)); + this.canvas.addEventListener('mousemove', this.handleCanvasMouseMove.bind(this)); + this.listenersAttached = true; + } + } + + reset() { + this.map = Object.create(null); + this.mapKeys = []; + this.dll = []; + this.dllConnections = []; + this.inspectedKey = null; + this.nextIndex = this.resetIndex; + } + + // Interactive Click-to-Inspect + + getCanvasCoords(event) { + if (!this.canvas) return { x: -1, y: -1 }; + const rect = this.canvas.getBoundingClientRect(); + const scaleX = this.canvas.width / rect.width; + const scaleY = this.canvas.height / rect.height; + return { + x: (event.clientX - rect.left) * scaleX, + y: (event.clientY - rect.top) * scaleY, + }; + } + + findHitRow(x, y) { + for (let i = 0; i < this.mapKeys.length; i++) { + const rowY = this.mapRowY(i); + const left = MAP_COL_KEY_X - MAP_KEY_W / 2; + const right = MAP_COL_REF_X + MAP_REF_W / 2; + const top = rowY - MAP_ROW_H / 2; + const bottom = rowY + MAP_ROW_H / 2; + if (x >= left && x <= right && y >= top && y <= bottom) { + return i; + } + } + return -1; + } + + handleCanvasMouseMove(event) { + if (!this.canvas) return; + const { x, y } = this.getCanvasCoords(event); + const hit = this.findHitRow(x, y); + this.canvas.style.cursor = hit !== -1 ? 'pointer' : 'default'; + } + + handleCanvasClick(event) { + if (this.animationManager && this.animationManager.currentlyAnimating) return; + const { x, y } = this.getCanvasCoords(event); + const hitRow = this.findHitRow(x, y); + + if (hitRow === -1) { + if (this.inspectedKey) this.clearInspection(); + return; + } + + const key = this.mapKeys[hitRow]; + if (this.inspectedKey === key) { + this.clearInspection(); + } else { + this.inspectKey(key); + } + } + + inspectKey(key) { + this.clearInspection(); + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + if (!mapEntry || dllIdx === -1) return; + + const targetNode = this.dll[dllIdx]; + this.inspectedKey = key; + this.commands = []; + + this.highlightEntryAndNode(key, PTR_CELL_BG, '#E1F5FE', MAP_LINK_COLOR); + + const posDesc = + dllIdx === 0 + ? 'index 0 (MRU)' + : dllIdx === this.dll.length - 1 + ? `index ${dllIdx} (LRU)` + : `index ${dllIdx}`; + this.setStatus(STATUS.INSPECT(key, targetNode.value, posDesc)); + + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.commands = []; + } + + clearInspection() { + if (!this.inspectedKey) return; + const key = this.inspectedKey; + this.inspectedKey = null; + + this.commands = []; + this.resetEntryAndNode(key); + this.setStatus(''); + this.animationManager.startNewAnimation(this.commands); + this.animationManager.skipForward(); + this.commands = []; + } + + // UI lifecycle + + enableUI() { + for (let i = 0; i < this.controls.length; i++) { + this.controls[i].disabled = false; + } + } + + disableUI() { + for (let i = 0; i < this.controls.length; i++) { + this.controls[i].disabled = true; + } + } + + // Helpers + + setStatus(text) { + this.cmd(act.setText, this.infoLabelID, text); + } + + capText() { + return `Capacity: ${this.dll ? this.dll.length : 0} / ${this.capacity}`; + } + + dllNodeX(idx) { + return DLL_START_X + idx * DLL_X_GAP; + } + + mapRowY(idx) { + return MAP_START_Y + idx * MAP_ROW_GAP; + } + + getDllIndex(key) { + return this.dll.findIndex(n => n.key === key); + } + + highlightEntryAndNode( + key, + cellColor, + nodeColor = cellColor, + linkColor = MAP_LINK_COLOR, + keyCellColor = cellColor, + ) { + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + if (!mapEntry || dllIdx === -1) return; + + const targetNodeID = this.dll[dllIdx].nodeID; + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, keyCellColor); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, cellColor); + this.cmd(act.setBackgroundColor, targetNodeID, nodeColor); + this.cmd(act.connect, mapEntry.ptrCellID, targetNodeID, linkColor, 0.0, true, '', 0); + } + + resetEntryAndNode(key, disconnect = true) { + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + + if (mapEntry) { + this.cmd(act.setBackgroundColor, mapEntry.keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, mapEntry.ptrCellID, PTR_CELL_BG); + if (dllIdx !== -1) { + const targetNodeID = this.dll[dllIdx].nodeID; + if (disconnect) { + this.cmd(act.disconnect, mapEntry.ptrCellID, targetNodeID); + } + this.cmd(act.setBackgroundColor, targetNodeID, WHITE); + } + } + } + + updateRecencyBadges() { + if (this.dll.length === 0) { + // Hide badges offscreen + this.cmd(act.move, this.mruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.mruTextID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else if (this.dll.length === 1) { + // Capacity = 1 / Size = 1 edge case: single combined badge + const x = this.dllNodeX(0); + this.cmd(act.setText, this.mruTextID, 'MRU / LRU'); + this.cmd(act.setWidth, this.mruBadgeID, 72); + this.cmd(act.setBackgroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, COMBO_BADGE_COLOR); + this.cmd(act.move, this.mruBadgeID, x, BADGE_Y); + this.cmd(act.move, this.mruTextID, x, BADGE_Y); + + // Hide secondary LRU badge + this.cmd(act.move, this.lruBadgeID, OFFSCREEN, OFFSCREEN); + this.cmd(act.move, this.lruTextID, OFFSCREEN, OFFSCREEN); + } else { + // Size > 1: separate MRU and LRU badges + const mruX = this.dllNodeX(0); + const lruX = this.dllNodeX(this.dll.length - 1); + + this.cmd(act.setText, this.mruTextID, 'MRU'); + this.cmd(act.setWidth, this.mruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.setForegroundColor, this.mruBadgeID, MRU_COLOR); + this.cmd(act.move, this.mruBadgeID, mruX, BADGE_Y); + this.cmd(act.move, this.mruTextID, mruX, BADGE_Y); + + this.cmd(act.setText, this.lruTextID, 'LRU'); + this.cmd(act.setWidth, this.lruBadgeID, BADGE_W); + this.cmd(act.setBackgroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.setForegroundColor, this.lruBadgeID, LRU_COLOR); + this.cmd(act.move, this.lruBadgeID, lruX, BADGE_Y); + this.cmd(act.move, this.lruTextID, lruX, BADGE_Y); + } + } + + // Callbacks for the supported operations of LRU Cache + + rejectInput(message, button) { + this.implementAction(this.setStatusCmd.bind(this), message); + this.shake(button); + } + + putCallback() { + this.clearInspection(); + const key = this.putKeyField.value.trim(); + const val = this.putValField.value.trim(); + if (key === '' || val === '') { + this.rejectInput(STATUS.NEED_KEY_VALUE, this.putButton); + return; + } + if (FORBIDDEN_KEYS.has(key)) { + this.rejectInput(STATUS.FORBIDDEN_KEY(key), this.putButton); + return; + } + this.putKeyField.value = ''; + this.putValField.value = ''; + this.implementAction(this.animatePut.bind(this), key, val); + } + + getCallback() { + this.clearInspection(); + const key = this.getKeyField.value.trim(); + if (key === '') { + this.rejectInput(STATUS.NEED_GET_KEY, this.getButton); + return; + } + this.getKeyField.value = ''; + this.implementAction(this.animateGet.bind(this), key); + } + + deleteCallback() { + this.clearInspection(); + const key = this.deleteKeyField.value.trim(); + if (key === '') { + this.rejectInput(STATUS.NEED_DEL_KEY, this.deleteButton); + return; + } + this.deleteKeyField.value = ''; + this.implementAction(this.animateDelete.bind(this), key); + } + + restartCallback() { + this.clearInspection(); + const raw = parseInt(this.capacityField.value, 10); + // Enforce capacity between 1 and MAX_CAPACITY (explicitly disallow 0 or negative) + if (isNaN(raw) || raw < 1 || raw > MAX_CAPACITY) { + this.rejectInput(STATUS.INVALID_CAPACITY(MAX_CAPACITY), this.restartButton); + return; + } + this.capacity = raw; + this.implementAction(this.clearAll.bind(this)); + } + + clearCallback() { + this.clearInspection(); + this.implementAction(this.clearAll.bind(this)); + } + + setStatusCmd(text) { + this.commands = []; + this.setStatus(text); + return this.commands; + } + + // Animation for put + + animatePut(key, value) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'put'); + this.cmd(act.step); + + if (key in this.map) { + this.highlight(1, 0, 'put'); + this.cmd(act.step); + + const dllIdx = this.getDllIndex(key); + + // Highlight Hash Map row & DLL node and draw active focused lookup pointer + this.highlightEntryAndNode(key, HIGHLIGHT_COLOR); + + this.highlight(2, 0, 'put'); + this.setStatus(STATUS.PUT_HIT(key, value)); + this.cmd(act.step); + + // Update value + this.dll[dllIdx].value = value; + this.cmd(act.setText, this.dll[dllIdx].valLabelID, value); + this.highlight(3, 0, 'put'); + this.cmd(act.step); + + // Move to head (MRU) + this.highlight(4, 0, 'put'); + this.setStatus(STATUS.MOVE_TO_MRU(key)); + this.moveToHead(dllIdx); + this.cmd(act.step); + + // Reset colors & remove active pointer arrow + this.resetEntryAndNode(key); + } else { + // Key is new + this.highlight(5, 0, 'put'); + this.cmd(act.step); + + // Evict LRU if at capacity + if (this.dll.length >= this.capacity) { + this.highlight(9, 0, 'put'); + this.cmd(act.step); + + const tail = this.dll[this.dll.length - 1]; + this.highlightEntryAndNode(tail.key, EVICT_COLOR, EVICT_COLOR, EVICT_COLOR); + this.setStatus(STATUS.PUT_EVICT(this.capacity, tail.key)); + this.highlight(10, 0, 'put'); + this.cmd(act.step); + + const tailMap = this.map[tail.key]; + if (tailMap) { + this.cmd(act.disconnect, tailMap.ptrCellID, tail.nodeID); + } + this.evictLRU(); + this.highlight(11, 0, 'put'); + this.highlight(12, 0, 'put'); + this.cmd(act.step); + } + + this.highlight(6, 0, 'put'); + this.setStatus(STATUS.PUT_INSERT(key, value)); + this.insertHead(key, value); + this.highlight(7, 0, 'put'); + this.highlight(8, 0, 'put'); + this.cmd(act.step); + } + + this.unhighlightAll('put'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.PUT_COMPLETE(key, value)); + return this.commands; + } + + // Animation for get + + animateGet(key) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'get'); + this.cmd(act.step); + + if (!(key in this.map)) { + this.highlight(1, 0, 'get'); + this.setStatus(STATUS.GET_MISS(key)); + this.cmd(act.step); + this.unhighlightAll('get'); + return this.commands; + } + + this.highlight(2, 0, 'get'); + const dllIdx = this.getDllIndex(key); + + // Flash Hash Map row & DLL node in green with active focused lookup arrow + this.highlightEntryAndNode(key, FOUND_COLOR); + + this.setStatus(STATUS.GET_HIT(key)); + this.cmd(act.step); + + this.highlight(3, 0, 'get'); + this.setStatus(STATUS.MOVE_TO_MRU(key)); + this.moveToHead(dllIdx); + this.cmd(act.step); + + // Disconnect active pointer and reset colors + this.resetEntryAndNode(key); + + this.unhighlightAll('get'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.GET_COMPLETE(key, this.dll[0].value)); + return this.commands; + } + + // Animation for delete + + animateDelete(key) { + this.commands = []; + this.setStatus(''); + this.highlight(0, 0, 'delete'); + this.cmd(act.step); + + if (!(key in this.map)) { + this.highlight(1, 0, 'delete'); + this.setStatus(STATUS.DEL_MISS(key)); + this.cmd(act.step); + this.unhighlightAll('delete'); + return this.commands; + } + + this.highlight(2, 0, 'delete'); + this.highlightEntryAndNode(key, EVICT_COLOR, EVICT_COLOR, EVICT_COLOR); + + this.setStatus(STATUS.DEL_FOUND(key)); + this.cmd(act.step); + + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + if (mapEntry && dllIdx !== -1) { + this.cmd(act.disconnect, mapEntry.ptrCellID, this.dll[dllIdx].nodeID); + } + this.highlight(3, 0, 'delete'); + this.removeEntry(key); + this.cmd(act.step); + + this.unhighlightAll('delete'); + this.redrawMapAndDLL(); + this.setStatus(STATUS.DEL_COMPLETE(key)); + return this.commands; + } + + // Core internal mutations + + insertHead(key, value) { + const nodeID = this.nextIndex++; + const valLabelID = this.nextIndex++; + const keyCellID = this.nextIndex++; + const ptrCellID = this.nextIndex++; + + const dllX = this.dllNodeX(0); + const dropY = DLL_START_Y - 50; + + // 1. Create DLL node (authentic 3-compartment doubly linked list node) + this.cmd(act.createDoublyLinkedListNode, nodeID, key, DLL_NODE_W, DLL_NODE_H, dllX, dropY); + this.cmd(act.setPrevNull, nodeID, 1); + this.cmd(act.setNextNull, nodeID, 1); + this.cmd(act.createLabel, valLabelID, value, dllX, dropY + VAL_OFFSET_Y, 1); + this.cmd(act.setForegroundColor, valLabelID, '#444444'); + + // 2. Create Hash Map entry row + const rowIdx = this.mapKeys.length; + const mapY = this.mapRowY(rowIdx); + + this.cmd(act.createRectangle, keyCellID, key, MAP_KEY_W, MAP_ROW_H, MAP_COL_KEY_X, mapY); + this.cmd(act.setBackgroundColor, keyCellID, KEY_CELL_BG); + this.cmd(act.setForegroundColor, keyCellID, CELL_BORDER); + + this.cmd(act.createRectangle, ptrCellID, '•', MAP_REF_W, MAP_ROW_H, MAP_COL_REF_X, mapY); + this.cmd(act.setBackgroundColor, ptrCellID, PTR_CELL_BG); + this.cmd(act.setForegroundColor, ptrCellID, CELL_BORDER); + this.cmd(act.setTextColor, ptrCellID, MAP_LINK_COLOR); + + // 3. Connect active pointer during insertion step + this.cmd(act.connect, ptrCellID, nodeID, MAP_LINK_COLOR, 0.0, true, '', 0); + + // Flash gold to emphasize insertion + this.cmd(act.setBackgroundColor, nodeID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, keyCellID, HIGHLIGHT_COLOR); + this.cmd(act.setBackgroundColor, ptrCellID, HIGHLIGHT_COLOR); + this.cmd(act.step); + + // Disconnect active pointer and reset colors back to standard + this.cmd(act.disconnect, ptrCellID, nodeID); + this.cmd(act.setBackgroundColor, nodeID, WHITE); + this.cmd(act.setBackgroundColor, keyCellID, KEY_CELL_BG); + this.cmd(act.setBackgroundColor, ptrCellID, PTR_CELL_BG); + + // Record in state + this.map[key] = { keyCellID, ptrCellID, nodeID, valLabelID }; + this.mapKeys.push(key); + this.dll.unshift({ key, value, nodeID, valLabelID }); + } + + moveToHead(srcIdx) { + if (srcIdx === 0) return; + const node = this.dll.splice(srcIdx, 1)[0]; + this.dll.unshift(node); + } + + evictLRU() { + if (this.dll.length === 0) return; + const tail = this.dll[this.dll.length - 1]; + this.removeEntry(tail.key); + } + + removeEntry(key) { + const mapEntry = this.map[key]; + const dllIdx = this.getDllIndex(key); + + if (mapEntry) { + this.cmd(act.delete, mapEntry.keyCellID); + this.cmd(act.delete, mapEntry.ptrCellID); + delete this.map[key]; + this.mapKeys = this.mapKeys.filter(k => k !== key); + } + + if (dllIdx !== -1) { + const node = this.dll[dllIdx]; + this.cmd(act.delete, node.nodeID); + this.cmd(act.delete, node.valLabelID); + this.dll.splice(dllIdx, 1); + } + } + + // Redraw & layout synchronization + + redrawMapAndDLL() { + // 1. Clear previous DLL connections + for (const conn of this.dllConnections) { + this.cmd(act.disconnect, conn.from, conn.to); + } + this.dllConnections = []; + + // 2. Reposition DLL nodes and value labels + for (let i = 0; i < this.dll.length; i++) { + const x = this.dllNodeX(i); + this.cmd(act.move, this.dll[i].nodeID, x, DLL_START_Y); + this.cmd(act.move, this.dll[i].valLabelID, x, DLL_START_Y + VAL_OFFSET_Y); + } + + // 3. Set DLL null pointers and bi-directional edges + for (let i = 0; i < this.dll.length; i++) { + this.cmd(act.setPrevNull, this.dll[i].nodeID, i === 0 ? 1 : 0); + this.cmd(act.setNextNull, this.dll[i].nodeID, i === this.dll.length - 1 ? 1 : 0); + } + for (let i = 0; i < this.dll.length - 1; i++) { + const from = this.dll[i].nodeID; + const to = this.dll[i + 1].nodeID; + this.cmd(act.connectNext, from, to); + this.dllConnections.push({ from, to }); + this.cmd(act.connectPrev, to, from); + this.dllConnections.push({ from: to, to: from }); + } + + // 4. Reposition Hash Map rows cleanly + for (let i = 0; i < this.mapKeys.length; i++) { + const k = this.mapKeys[i]; + const entry = this.map[k]; + if (entry) { + const rowY = this.mapRowY(i); + this.cmd(act.move, entry.keyCellID, MAP_COL_KEY_X, rowY); + this.cmd(act.move, entry.ptrCellID, MAP_COL_REF_X, rowY); + } + } + + // 5. Update MRU / LRU Badges + this.updateRecencyBadges(); + + // 6. Update capacity counter + this.cmd(act.setText, this.capLabelID, this.capText()); + this.cmd(act.step); + } + + unhighlightAll(op) { + const lines = (this.pseudocode && this.pseudocode[op] && this.pseudocode[op].code) || []; + for (let i = 0; i < lines.length; i++) { + this.unhighlight(i, 0, op); + } + } + + // Clear / reset + + clearAll() { + this.commands = []; + this.setStatus(''); + + this.dllConnections = []; + + // Destroy all DLL nodes and their value labels + for (let i = 0; i < this.dll.length; i++) { + this.cmd(act.delete, this.dll[i].nodeID); + this.cmd(act.delete, this.dll[i].valLabelID); + } + this.dll = []; + + // Destroy all Hash Map rows + for (const k of this.mapKeys) { + const entry = this.map[k]; + if (entry) { + this.cmd(act.delete, entry.keyCellID); + this.cmd(act.delete, entry.ptrCellID); + } + } + this.map = Object.create(null); + this.mapKeys = []; + this.inspectedKey = null; + + // Hide badges + this.updateRecencyBadges(); + + // Reset capacity counter + this.cmd(act.setText, this.capLabelID, this.capText()); + + return this.commands; + } +} diff --git a/src/algo/__tests__/LRU.test.js b/src/algo/__tests__/LRU.test.js new file mode 100644 index 0000000..413e3ea --- /dev/null +++ b/src/algo/__tests__/LRU.test.js @@ -0,0 +1,339 @@ +import LRU, { STATUS } from '../LRU'; +import pseudocodeText from '../../pseudocode.json'; +import timeComplexities from '../../time_complexities.json'; + +describe('LRU Cache metadata', () => { + test('LRU pseudocode is defined for put, get, and delete', () => { + expect(pseudocodeText.LRU).toBeDefined(); + expect(pseudocodeText.LRU.put).toBeDefined(); + expect(pseudocodeText.LRU.get).toBeDefined(); + expect(pseudocodeText.LRU.delete).toBeDefined(); + + expect(pseudocodeText.LRU.put.code.length).toBeGreaterThan(0); + expect(pseudocodeText.LRU.get.code.length).toBeGreaterThan(0); + expect(pseudocodeText.LRU.delete.code.length).toBeGreaterThan(0); + }); + + test('LRU time complexities are properly defined', () => { + expect(timeComplexities.LRU).toBeDefined(); + expect(timeComplexities.LRU['LRU Cache']).toBeDefined(); + const ops = timeComplexities.LRU['LRU Cache']; + expect(ops['put (no eviction)'].big_o).toBe('O(1)'); + expect(ops['put (with eviction)'].big_o).toBe('O(1)'); + expect(ops['get'].big_o).toBe('O(1)'); + expect(ops['delete'].big_o).toBe('O(1)'); + }); + + test('STATUS dictionary formats expected status messages', () => { + expect(STATUS.NEED_KEY_VALUE).toBe('Please enter both a key and a value.'); + expect(STATUS.FORBIDDEN_KEY('__proto__')).toContain('__proto__'); + expect(STATUS.INVALID_CAPACITY(10)).toContain('between 1 and 10'); + expect(STATUS.PUT_HIT('k1', 'v1')).toContain('Cache Hit on key "k1"'); + expect(STATUS.PUT_EVICT(5, 'k_old')).toContain('evicting LRU key "k_old"'); + expect(STATUS.GET_MISS('missing')).toContain('Cache Miss'); + expect(STATUS.GET_HIT('k1')).toContain('Cache Hit'); + expect(STATUS.DEL_COMPLETE('k1')).toContain('delete("k1") complete'); + expect(STATUS.INSPECT('k1', 'v1', 'index 0 (MRU)')).toContain('index 0 (MRU)'); + }); +}); + +describe('LRU Visualization Class (LRU.js)', () => { + let mockAm; + let lru; + + beforeEach(() => { + document.body.innerHTML = ` +
+
+ + `; + + mockAm = { + addListener: jest.fn(), + startNewAnimation: jest.fn(), + setAnimationDelay: jest.fn(), + skipForward: jest.fn(), + clearHistory: jest.fn(), + currentlyAnimating: false, + }; + + lru = new LRU(mockAm, 1000, 600); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + test('initializes default state, controls, and canvas structures', () => { + expect(lru.capacity).toBe(5); + expect(lru.map).toBeDefined(); + expect(Object.keys(lru.map).length).toBe(0); + expect(lru.dll).toEqual([]); + expect(lru.controls.length).toBeGreaterThan(0); + expect(lru.putKeyField).toBeDefined(); + expect(lru.putValField).toBeDefined(); + expect(lru.putButton).toBeDefined(); + expect(lru.getKeyField).toBeDefined(); + expect(lru.getButton).toBeDefined(); + expect(lru.deleteKeyField).toBeDefined(); + expect(lru.deleteButton).toBeDefined(); + }); + + test('animatePut inserts into cache map and doubly linked list at MRU', () => { + lru.animatePut('k1', 'v1'); + + expect(lru.map['k1']).toBeDefined(); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('k1'); + expect(lru.dll[0].value).toBe('v1'); + + // Insert second element + lru.animatePut('k2', 'v2'); + expect(lru.dll.length).toBe(2); + expect(lru.dll[0].key).toBe('k2'); // MRU + expect(lru.dll[1].key).toBe('k1'); // LRU + }); + + test('animateGet on existing key moves it to MRU (head of list)', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + lru.animatePut('k3', 'v3'); + // Initial order: k3 (MRU), k2, k1 (LRU) + expect(lru.dll.map(n => n.key)).toEqual(['k3', 'k2', 'k1']); + + const cmds = lru.animateGet('k1'); + expect(cmds.length).toBeGreaterThan(0); + // After get(k1), k1 must become MRU + expect(lru.dll.map(n => n.key)).toEqual(['k1', 'k3', 'k2']); + }); + + test('animateGet on nonexistent key leaves list unchanged', () => { + lru.animatePut('k1', 'v1'); + const cmds = lru.animateGet('nonexistent'); + expect(cmds.length).toBeGreaterThan(0); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('k1'); + }); + + test('animatePut updating an existing key updates value and promotes to MRU without increasing size', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + expect(lru.dll.length).toBe(2); + + lru.animatePut('k1', 'v1_updated'); + expect(lru.dll.length).toBe(2); + expect(lru.dll[0].key).toBe('k1'); + expect(lru.dll[0].value).toBe('v1_updated'); + expect(lru.dll[1].key).toBe('k2'); + }); + + test('evicts the LRU tail node when capacity is exceeded', () => { + lru.capacity = 2; + lru.animatePut('1', 'one'); + lru.animatePut('2', 'two'); + expect(lru.dll.map(n => n.key)).toEqual(['2', '1']); + + // Access '1' to make '2' the LRU node + lru.animateGet('1'); + expect(lru.dll.map(n => n.key)).toEqual(['1', '2']); + + // Put '3' -> evicts '2' + lru.animatePut('3', 'three'); + expect(lru.map['2']).toBeUndefined(); + expect(lru.map['1']).toBeDefined(); + expect(lru.map['3']).toBeDefined(); + expect(lru.dll.map(n => n.key)).toEqual(['3', '1']); + }); + + test('animateDelete removes element from cache map and dll', () => { + lru.animatePut('1', 'a'); + lru.animatePut('2', 'b'); + expect(lru.dll.length).toBe(2); + + lru.animateDelete('1'); + expect(lru.map['1']).toBeUndefined(); + expect(lru.dll.length).toBe(1); + expect(lru.dll[0].key).toBe('2'); + + // Deleting nonexistent key causes no errors + const cmds = lru.animateDelete('nonexistent'); + expect(cmds.length).toBeGreaterThan(0); + expect(lru.dll.length).toBe(1); + }); + + test('handles inspection mode and clearInspection', () => { + lru.animatePut('k1', 'v1'); + lru.animatePut('k2', 'v2'); + + lru.inspectKey('k1'); + expect(lru.inspectedKey).toBe('k1'); + + lru.clearInspection(); + expect(lru.inspectedKey).toBeNull(); + }); + + test('putCallback processes valid input and clears fields', () => { + lru.putKeyField.value = 'userKey'; + lru.putValField.value = 'userVal'; + lru.putCallback(); + + expect(lru.map['userKey']).toBeDefined(); + expect(lru.dll[0].key).toBe('userKey'); + expect(lru.dll[0].value).toBe('userVal'); + expect(lru.putKeyField.value).toBe(''); + expect(lru.putValField.value).toBe(''); + }); + + test('putCallback with empty inputs shakes button and shows error', () => { + lru.putKeyField.value = ''; + lru.putValField.value = ''; + lru.putCallback(); + + expect(lru.putButton.classList.contains('shake')).toBe(true); + expect(mockAm.startNewAnimation).toHaveBeenCalled(); + }); + + test('getCallback processes valid key, promotes to MRU, and clears field', () => { + lru.animatePut('g1', 'v1'); + lru.animatePut('g2', 'v2'); + + lru.getKeyField.value = 'g1'; + lru.getCallback(); + + expect(lru.dll[0].key).toBe('g1'); + expect(lru.getKeyField.value).toBe(''); + }); + + test('getCallback with empty key shakes button and shows error', () => { + lru.getKeyField.value = ''; + lru.getCallback(); + + expect(lru.getButton.classList.contains('shake')).toBe(true); + }); + + test('deleteCallback processes valid key and clears field', () => { + lru.animatePut('d1', 'v1'); + lru.deleteKeyField.value = 'd1'; + lru.deleteCallback(); + + expect(lru.map['d1']).toBeUndefined(); + expect(lru.dll.length).toBe(0); + expect(lru.deleteKeyField.value).toBe(''); + }); + + test('deleteCallback with empty key shakes button and shows error', () => { + lru.deleteKeyField.value = ''; + lru.deleteCallback(); + + expect(lru.deleteButton.classList.contains('shake')).toBe(true); + }); + + test('clearCallback empties cache while preserving capacity setting', () => { + lru.capacity = 3; + lru.animatePut('c1', 'v1'); + lru.animatePut('c2', 'v2'); + expect(lru.dll.length).toBe(2); + + lru.clearCallback(); + + expect(lru.capacity).toBe(3); + expect(lru.dll.length).toBe(0); + expect(Object.keys(lru.map).length).toBe(0); + expect(lru.inspectedKey).toBeNull(); + }); + + test('canvas mousemove updates cursor style based on hit testing', () => { + lru.animatePut('m1', 'v1'); + lru.canvas.getBoundingClientRect = () => ({ + left: 0, + top: 0, + width: 1000, + height: 600, + }); + + // Hover directly over row 0 (MAP_START_Y = 130) + lru.handleCanvasMouseMove({ clientX: 48, clientY: 130 }); + expect(lru.canvas.style.cursor).toBe('pointer'); + + // Hover outside row 0 + lru.handleCanvasMouseMove({ clientX: 500, clientY: 500 }); + expect(lru.canvas.style.cursor).toBe('default'); + }); + + test('canvas click toggles inspection and handles outside click', () => { + lru.animatePut('c1', 'v1'); + lru.canvas.getBoundingClientRect = () => ({ + left: 0, + top: 0, + width: 1000, + height: 600, + }); + + // Click on row 0 -> inspects c1 + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBe('c1'); + + // Click same row again -> dismisses inspection (toggle off) + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBeNull(); + + // Click on row 0 again -> inspects + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBe('c1'); + + // Click outside -> dismisses inspection + lru.handleCanvasClick({ clientX: 500, clientY: 500 }); + expect(lru.inspectedKey).toBeNull(); + + // Clicking while animating is ignored + lru.animationManager.currentlyAnimating = true; + lru.handleCanvasClick({ clientX: 48, clientY: 130 }); + expect(lru.inspectedKey).toBeNull(); + lru.animationManager.currentlyAnimating = false; + }); + + test('starting any operation automatically dismisses active inspection', () => { + lru.animatePut('x1', 'v1'); + lru.inspectKey('x1'); + expect(lru.inspectedKey).toBe('x1'); + + // Starting a putCallback dismisses inspection + lru.putKeyField.value = 'x2'; + lru.putValField.value = 'v2'; + lru.putCallback(); + expect(lru.inspectedKey).toBeNull(); + }); + + test('rejects invalid capacity inputs during restartCallback', () => { + lru.capacity = 3; + + // Test 0 + lru.capacityField.value = '0'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); // unchanged + + // Test negative + lru.capacityField.value = '-5'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); + + // Test > 10 + lru.capacityField.value = '11'; + lru.restartCallback(); + expect(lru.capacity).toBe(3); + + // Test valid capacity + lru.capacityField.value = '7'; + lru.restartCallback(); + expect(lru.capacity).toBe(7); + }); + + test('rejects forbidden prototype keys in putCallback', () => { + lru.putKeyField.value = '__proto__'; + lru.putValField.value = 'danger'; + lru.putCallback(); + + expect(lru.map['__proto__']).toBeUndefined(); + expect(lru.dll.length).toBe(0); + }); +}); diff --git a/src/algo/__tests__/index.test.js b/src/algo/__tests__/index.test.js index a5d0c32..16f199d 100644 --- a/src/algo/__tests__/index.test.js +++ b/src/algo/__tests__/index.test.js @@ -30,6 +30,7 @@ describe('algo index exports', () => { 'KMP', 'Kruskals', 'LCS', + 'LRU', 'LSDRadix', 'LVA', 'LinkedList', diff --git a/src/algo/index.js b/src/algo/index.js index 475b55a..f89c9f6 100644 --- a/src/algo/index.js +++ b/src/algo/index.js @@ -14,6 +14,7 @@ export { default as LVA } from './LVA'; export { default as BogoSort } from './BogoSort'; export { default as NonLinearProbing } from './NonLinearProbing'; export { default as LCS } from './LCS'; +export { default as LRU } from './LRU'; export { default as DequeArray } from './DequeArray'; export { default as DequeLL } from './DequeLL'; export { default as Dijkstras } from './Dijkstras'; diff --git a/src/pseudocode.json b/src/pseudocode.json index ab0046e..f8ccfae 100644 --- a/src/pseudocode.json +++ b/src/pseudocode.json @@ -1,4 +1,74 @@ { + "LRU": { + "put": { + "code": [ + ["procedure put(key, value):"], + [" if key in map:"], + [" node ← map[key]"], + [" update node.value ← value"], + [" move node to head (MRU)"], + [" else:"], + [" create new node(key, value)"], + [" insert node at head"], + [" map[key] ← node"], + [" if size > capacity:"], + [" evict ← tail (LRU node)"], + [" remove evict from list"], + [" delete map[evict.key]"], + ["end procedure"] + ], + "english": [ + ["procedure put(key, value):"], + [" if key already exists in cache:"], + [" look up the node in the cache"], + [" update its value"], + [" move it to the front (MRU)"], + [" otherwise:"], + [" create a new cache entry"], + [" insert at front of list"], + [" record it in the cache"], + [" if over capacity:"], + [" identify the tail (LRU entry)"], + [" remove it from the list"], + [" remove it from the cache"], + ["end procedure"] + ] + }, + "get": { + "code": [ + ["procedure get(key):"], + [" if key not in map: return -1"], + [" node ← map[key]"], + [" move node to head (MRU)"], + [" return node.value"], + ["end procedure"] + ], + "english": [ + ["procedure get(key):"], + [" if key not in cache: return -1"], + [" look up the node in the cache"], + [" move the node to the front (MRU)"], + [" return the node's value"], + ["end procedure"] + ] + }, + "delete": { + "code": [ + ["procedure delete(key):"], + [" if key not in map: return"], + [" node ← map[key]"], + [" remove node from list; delete map[key]"], + ["end procedure"] + ], + "english": [ + ["procedure delete(key):"], + [" if key not in cache: return"], + [" find the node in the cache"], + [" remove from list and cache"], + ["end procedure"] + ] + } + }, "ArrayList": { "addFB": { "code": [ diff --git a/src/time_complexities.json b/src/time_complexities.json index 5608b26..f172863 100644 --- a/src/time_complexities.json +++ b/src/time_complexities.json @@ -1560,5 +1560,25 @@ "explanation": "Each letter in the first string is compared to each letter of the second string." } } + }, + "LRU": { + "LRU Cache": { + "put (no eviction)": { + "big_o": "O(1)", + "explanation": "HashMap lookup + doubly-linked list head insertion are both O(1)." + }, + "put (with eviction)": { + "big_o": "O(1)", + "explanation": "Tail removal from the DLL + HashMap delete are both O(1)." + }, + "get": { + "big_o": "O(1)", + "explanation": "HashMap lookup finds the node in O(1); moving it to head in the DLL is also O(1)." + }, + "delete": { + "big_o": "O(1)", + "explanation": "HashMap lookup + DLL node removal are both O(1)." + } + } } }