LRU Cache (O(1))
LRU Cache — задача #146 на LeetCode, одна из самых популярных на системных и frontend-собеседованиях. Интервьюер ждёт обе операции за O(1): можно использовать Map (iteration order trick) или Doubly Linked List + HashMap. Важно объяснить трейдоффы обоих подходов.
Подход 1 — Map-trick: порядок итерации Map совпадает с порядком вставки в JS:
// Map в JS: O(1) get/set/delete + порядок вставки = готовый LRU.// «Недавно использованный» = удалить и вставить в конец Map.
class LRUCacheMap { #capacity; #cache = new Map(); // oldest (первый) → newest (последний)
/** @param {number} capacity */ constructor(capacity) { if (capacity < 1) throw new RangeError('capacity >= 1'); this.#capacity = capacity; }
/** * @param {number} key * @returns {number} значение или -1 если отсутствует * O(1) амортизированно */ get(key) { if (!this.#cache.has(key)) return -1; const val = this.#cache.get(key); this.#cache.delete(key); // убрать со старой позиции this.#cache.set(key, val); // вставить в конец (newest) return val; }
/** * Вставить/обновить. При переполнении вытесняем LRU (первый = oldest). * O(1) амортизированно */ put(key, value) { if (this.#cache.has(key)) this.#cache.delete(key); // переставить в конец this.#cache.set(key, value); if (this.#cache.size > this.#capacity) { this.#cache.delete(this.#cache.keys().next().value); // удалить oldest } }
get size() { return this.#cache.size; }}
// Трейдоффы Map-подхода:// ✅ Компактный код (~25 строк), лёгкий для написания на доске// ✅ O(1) амортизированно для get/put// ⚠️ Зависит от внутренней реализации Map в движке (spec гарантирует порядок)// ⚠️ На вопрос "докажи O(1)" — чуть сложнее аргументировать чем DLLПодход 2 — Doubly Linked List + HashMap: явный O(1) без зависимости от Map:
// DLL: head (oldest) ↔ ... ↔ tail (newest), sentinel nodes// Map: key → ListNode → O(1) доступ к любому узлу
class DLLNode { constructor(key = 0, val = 0) { this.key = key; this.val = val; this.prev = null; this.next = null; }}
class LRUCacheDLL { #cap; #map = new Map(); #head = new DLLNode(); // sentinel oldest #tail = new DLLNode(); // sentinel newest
constructor(capacity) { this.#cap = capacity; this.#head.next = this.#tail; this.#tail.prev = this.#head; }
#addToTail(node) { // вставить перед tail node.prev = this.#tail.prev; node.next = this.#tail; this.#tail.prev.next = node; this.#tail.prev = node; }
#removeNode(node) { // вырезать из списка node.prev.next = node.next; node.next.prev = node.prev; }
get(key) { // O(1) if (!this.#map.has(key)) return -1; const node = this.#map.get(key); this.#removeNode(node); this.#addToTail(node); // переместить в newest return node.val; }
put(key, value) { // O(1) if (this.#map.has(key)) { const node = this.#map.get(key); node.val = value; this.#removeNode(node); this.#addToTail(node); } else { if (this.#map.size === this.#cap) { const lru = this.#head.next; // oldest — сразу после sentinel head this.#removeNode(lru); this.#map.delete(lru.key); } const node = new DLLNode(key, value); this.#addToTail(node); this.#map.set(key, node); } }}
// Трейдоффы DLL-подхода:// ✅ Явный O(1) — не зависит от внутренней реализации Map// ✅ Правильный ответ на "докажи O(1)" — pointer-операции// ⚠️ ~45 строк, больше шансов на ошибку в указателях на доскеЕдиные тесты для обоих классов:
function testLRU(LRU) { const cache = new LRU(2); cache.put(1, 1); // {1=1} cache.put(2, 2); // {1=1, 2=2} console.assert(cache.get(1) === 1); // {2=2, 1=1} ← 1 стал newest cache.put(3, 3); // вытеснить 2: {1=1, 3=3} console.assert(cache.get(2) === -1); // 2 вытеснен cache.put(4, 4); // вытеснить 1: {3=3, 4=4} console.assert(cache.get(1) === -1); // 1 вытеснен console.assert(cache.get(3) === 3); // {4=4, 3=3} console.assert(cache.get(4) === 4); // {3=3, 4=4} console.log('OK', LRU.name);}
testLRU(LRUCacheMap);testLRU(LRUCacheDLL);
// ─── Граничные случаи ─────────────────────────────────────────────────────// capacity = 1const single = new LRUCacheMap(1);single.put(1, 10);single.put(2, 20); // вытесняет 1console.assert(single.get(1) === -1);console.assert(single.get(2) === 20);
// Обновление существующего ключа не вызывает вытеснениеconst c = new LRUCacheMap(2);c.put(1, 1); c.put(2, 2);c.put(1, 10); // обновить 1, сделать его newestc.put(3, 3); // вытесняет 2 (oldest), не 1console.assert(c.get(1) === 10); // 1 не вытеснен ✓console.assert(c.get(2) === -1); // 2 вытеснен ✓Сложность
- Время: O(1) get / O(1) put для обоих подходов
- Память: O(capacity) — хранится не более capacity элементов
Итог: LRU за O(1): Map-trick (25 строк) или DLL+HashMap (45 строк, явный O(1)). На собеседовании показать оба и объяснить трейдофф.