Зачем предпочитать DoubleLinkedList вместо очереди и хэш-карты для разработки наименее недавно используемого (LRU)?

Решаю задачу дизайна Leetcode LRU - Leetcode LRU:

Спроектируйте структуру данных, которая соответствует ограничениям кэша Наименее недавно использованного (LRU) .

Реализуйте класс LRUCache:

  • LRUCache(int capacity) Инициализируйте кэш LRU с положительным размером capacity.
  • int get(int key) Верните значение key, если key существует, в противном случае верните -1.
  • void put(int key, int value) Обновите значение key, если key существует. В противном случае добавьте пару key-value в кеш. Если количество ключей превышает capacity в этой операции, удалите ключ, который использовался реже всего.

Каждая из функций get и put должна выполняться со средней временной сложностью O(1).

Я разработал его с использованием Queue и HashMap и смог пройти 20 из 22 тестовых случаев. Однако время ожидания остальных тестовых случаев истекло.

При поиске я обнаружил, что двусвязный список — лучший способ его реализовать. Мне любопытно, почему истекает время ожидания очереди и хеш-карты и почему двусвязный список — лучший способ решить эту проблему.

Ниже моя реализация:

class LRUCache {
    int capacity=0;
    BlockingQueue<Integer> queue;
    Map<Integer, Integer> map = new HashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        queue = new ArrayBlockingQueue<Integer>(capacity);
    }
    
    public int get(int key) {
        if (queue.contains(key)){
            queue.remove(key);
            queue.add(key);
            return map.get(key);
        }
        else
            return -1;
    }
    
    public void put(int key, int value) {
        if (queue.contains(key)){
            queue.remove(key);
            queue.add(key);
            map.put(key, value);
        }
        else if (queue.size()<capacity){
            queue.add(key);
            map.put(key,value);
            
        }
        else{
            int oldKey = queue.remove();
            map.remove(oldKey);
            queue.add(key);
            map.put(key,value);
        }
    }
}

Результат такой, как показано ниже:

Зачем предпочитать DoubleLinkedList вместо очереди и хэш-карты для разработки наименее недавно используемого (LRU)?

Как вы думаете, какова асимптотическая эффективность queue.remove(key) и queue.contains(key)?

Louis Wasserman 28.05.2024 08:43

O(n), насколько я понимаю. как это поможет? @Луи Вассерман

Karthik G Sarode 28.05.2024 09:00

@KarthikGSarode, в задаче по коду особо упоминается, что «каждый из get и put должен выполняться со средней временной сложностью O (1)».

trincot 28.05.2024 09:23
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
3
66
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вызовы методов queue.remove(key) и queue.contains(key) не имеют временной сложности O(1). См. документацию по ArrayBlockingQueue, в которой упоминается, что эта очередь «...поддерживается массивом», т. е. ей необходимо сканировать массив, чтобы найти заданное значение. Это имеет временную сложность O(𝑛). Это достаточная причина, чтобы не использовать его для этой задачи. Кроме того, операции используют блокировку, чтобы избежать проблем параллелизма, которые делают их еще медленнее. В документации по remove есть:

удалять

public boolean remove(Object o)

[...] Удаление внутренних элементов в очередях на основе кольцевых массивов — это по своей сути медленная и разрушительная операция, поэтому ее следует предпринимать только в исключительных обстоятельствах, в идеале только тогда, когда известно, что очередь недоступна для других потоков.

Двусвязный список

Вы упоминаете двусвязные списки: в Java даже есть реализация двусвязного списка, которая сочетается с хэш-картой: LinkedHashMap.

В документации упоминается:

Предусмотрен специальный конструктор для создания связанной хэш-карты, порядок итерации которой соответствует порядку последнего доступа к ее записям: от самого последнего обращения до самого последнего (порядок доступа). Такая карта хорошо подходит для создания кэшей LRU.

И это именно то, что вам здесь нужно. Реализация может быть:

class LRUCache extends LinkedHashMap<Integer, Integer> {
    int capacity;  
    
    public LRUCache(int capacity) {
        // Foresee one more than desired capacity, so no extension is needed
        // when we allow a temporary overrun before deleting the eldest entry
        super(capacity + 1, 1, true); // true will enable the LRU behavior
        this.capacity = capacity;
    }

    // This method is called internally by put, getOrDefault (and similar).
    //    See documentation
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> entry) {
        return this.size() > this.capacity; // overrun detected: ask for removal
    }
    
    public int get(int key) {
        return getOrDefault(key, -1);
    }
}

спасибо за ответ, и вы кодируйте!! БОГ, я никогда не видел такой короткой реализации. очень блестяще!

Karthik G Sarode 28.05.2024 11:38

Другие вопросы по теме