Skip to content

LRU Cache⚓︎

Link

Description⚓︎

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.

Implement the LRUCache class:

  • LRUCache(int capacity) Initialize the LRU cache with positive size capacity.
  • int get(int key) Return the value of the key if the key exists, otherwise return -1.
  • void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.

The functions get and put must each run in \(O(1)\) average time complexity.

Example 1:

Input:

["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]

Output:

[null, null, null, 1, null, -1, null, -1, 3, 4]

Explanation:

LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // cache is {1=1}
lRUCache.put(2, 2); // cache is {1=1, 2=2}
lRUCache.get(1);    // return 1
lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3}
lRUCache.get(2);    // returns -1 (not found)
lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3}
lRUCache.get(1);    // return -1 (not found)
lRUCache.get(3);    // return 3
lRUCache.get(4);    // return 4

Constraints:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10^4
  • 0 <= value <= 10^5
  • At most 2 * 10^5 calls will be made to get and put.

Solution⚓︎

Way 1⚓︎

See reference (Chinese).

struct LinkedNode {
    int key, value;
    LinkedNode* prev;
    LinkedNode* next;

    LinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
    LinkedNode(int _key, int _value) : key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, LinkedNode*> cache;
    LinkedNode* head;
    LinkedNode* tail;
    int size;
    int capacity;

    void addToHead(LinkedNode* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    void removeNode(LinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(LinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    LinkedNode* removeTail() {
        LinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }

public:
    LRUCache(int _capacity) : capacity(_capacity), size(0) {
        head = new LinkedNode();
        tail = new LinkedNode();
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        // if key does not exist, return -1
        if (!cache.count(key)) return -1;
        // if key exists, use hash map to locate, and move to head
        LinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (!cache.count(key)) {
            // if key does not exist, create a new node
            LinkedNode* node = new LinkedNode(key, value);
            // add to hash map
            cache[key] = node;
            // add to head of double linked list
            addToHead(node);
            size++;

            if (size > capacity) {
                // delete tail node if size is larger than capacity
                LinkedNode* removed = removeTail();
                cache.erase(removed->key);
                delete removed;
                size--;
            }
        } else {
            // if key exists, locate through hash map and move the modified node to head
            LinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */
  • Time complexity: \(O(1)\) for get and put;
  • Space complexity: \(O(\mathrm{capacity})\).

Analysis:

  • Initialization (LRUCache Constructor):
    • Initializes the dummy head and tail and links them. The cache starts empty with a specified capacity.
  • Get Operation (get Method):
  • If the key exists in the cache, the corresponding node is moved to the head of the list (indicating recent use) and its value is returned.
  • If the key is not present, returns -1.
  • Put Operation (put Method):
    • If the key is not in the cache, a new node is created and added to the head of the list. If this addition exceeds the capacity, the least recently used item (node before the tail) is removed.
    • If the key exists, its value is updated, and the node is moved to the head.

Note: we may also need to add the following deconstruction method to free the memory:

class LRUCache {
    // ... [rest of the existing code]

    ~LRUCache() {
        LinkedNode* current = head;
        while (current) {
            LinkedNode* temp = current;
            current = current->next;
            delete temp;
        }
    }
};

Way 2⚓︎

class LRUCache {
private:
    list<pair<int, int>> cache;  // pair[key] == value
    unordered_map<int, list<pair<int, int>>::iterator> keyToNode;
    int capacity;

public:
    LRUCache(int _capacity) : capacity(_capacity) {}

    int get(int key) {
        if (keyToNode.find(key) == keyToNode.end()) return -1;

        auto keyValue = *keyToNode[key];
        cache.erase(keyToNode[key]);
        cache.emplace_front(keyValue);
        keyToNode[key] = cache.begin();

        return keyValue.second;
    }

    void put(int key, int value) {
        if (keyToNode.find(key) != keyToNode.end()) {
            cache.erase(keyToNode[key]);
        } else {
            if (cache.size() == capacity) {
                keyToNode.erase(cache.back().first);
                cache.pop_back();
            }
        }

        cache.emplace_front(key, value);
        keyToNode[key] = cache.begin();
    }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */