← Назад к списку
ПрограммированиеАрхитектура и алгоритмыMiddle

Реализуйте LRU-кэш заданной ёмкости: get(key) возвращает значение или -1, put(key, value) добавляет пару и вытесняет самый давно не использованный элемент при переполнении. Обе операции — за O(1).

Короткий ответ

  • Нужны O(1) поиск и O(1) обновление порядка использования
  • Хэш-таблица даёт поиск, двусвязный список — порядок
  • Словарь хранит ссылки прямо на узлы списка
  • Использованный узел переносится в голову списка
  • Вытесняется узел из хвоста — самый давний
  • Фиктивные head и tail убирают граничные случаи
  • В Python это инкапсулирует OrderedDict

Хэш-таблица плюс двусвязный список (или OrderedDict) дают get и put за O(1) по времени при O(capacity) памяти.

Как сказать вслух

пример ответа

Ни одна структура в одиночку не даёт O(1) на обе операции, поэтому я комбинирую две: словарь для мгновенного поиска и двусвязный список для порядка использования. В словаре лежат ссылки на узлы списка, так что перенос узла в голову тоже O(1). В Python покажу компактный вариант на OrderedDict, но объясню, что у него внутри то же самое.

Подробный ответ

Основной ответ

Требование O(1) на get и put диктует комбинацию структур. Хэш-таблица отображает ключ в узел двусвязного списка; список упорядочен по свежести: голова — недавно использованные, хвост — кандидат на вытеснение. Get: найти узел через словарь, отцепить и перенести в голову, вернуть значение. Put: если ключ есть — обновить значение и перенести в голову; если нет — создать узел в голове, а при превышении ёмкости удалить хвостовой узел и его ключ из словаря. Двусвязность списка обязательна: для отцепления узла за O(1) нужна ссылка на предыдущий. Фиктивные граничные узлы head/tail избавляют от проверок на пустоту. В Python OrderedDict с move_to_end и popitem(last=False) реализует ровно это.

Ключевые моменты

  • Почему две структуры. Словарь не упорядочен, список не ищет за O(1); словарь со ссылками на узлы списка объединяет оба свойства.
  • Двусвязность. Удалить узел из середины за O(1) можно, только зная его prev — односвязный список потребовал бы поиска за O(n).
  • Get тоже двигает. Чтение — это использование: get обязан переносить узел в голову, иначе вытеснение выберет не того.
  • Sentinel-узлы. Фиктивные head и tail делают вставку и удаление единообразными — нет веток для пустого кэша и крайних узлов.

Практический контекст

Классика раундов design-coding: проверяется выбор структур данных под заданные сложности и аккуратность с указателями при самостоятельной реализации списка. Спросите, можно ли использовать OrderedDict/LinkedHashMap или нужна реализация с нуля. Будьте готовы к развитиям: потокобезопасность (мьютекс вокруг операций), TTL для записей, LFU-вытеснение как усложнение.

Пример кода

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.data = OrderedDict()  # порядок: от давнего к свежему

    def get(self, key):
        if key not in self.data:
            return -1
        self.data.move_to_end(key)  # пометить как свежий
        return self.data[key]

    def put(self, key, value):
        self.data[key] = value
        self.data.move_to_end(key)
        if len(self.data) > self.cap:
            self.data.popitem(last=False)  # вытеснить давний

c = LRUCache(2)
c.put(1, 1); c.put(2, 2)
assert c.get(1) == 1
c.put(3, 3)  # вытесняет ключ 2
assert c.get(2) == -1 and c.get(3) == 3

Частые ошибки

  • Забывают, что get тоже должен обновлять порядок — вытесняется недавно прочитанный элемент
  • Берут односвязный список и получают O(n) на удаление узла из середины
  • При вытеснении удаляют узел из списка, но забывают удалить ключ из словаря — утечка и рассинхрон

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru