LRU Cache

Medium Top 250
Interviewed At (Company Tags)
GoogleAmazonMicrosoftMetaApple

Problem 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.


Examples

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]


Constraints

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

Hash Map and Doubly Linked List

To achieve O(1) time complexity for both get and put operations:

  1. We use a Hash Map to lookup nodes by their key in O(1) time.
  2. We use a Doubly Linked List to maintain the order of elements based on recency.
    • The head of the list points to the most recently used (MRU) node.
    • The tail of the list points to the least recently used (LRU) node. When a key is accessed (via get or put), we lookup its node in the hash map, disconnect it from its current position in the list, and move it to the head of the list. When inserting a new key that exceeds capacity, we evict the node at the tail of the list, remove its entry from the hash map, and insert the new node at the head.

Solution 1: Hash Map with Doubly Linked List

Pair a hash map with a doubly linked list to support O(1) cache lookups and updates.

import java.util.HashMap;

class LRUCache {
    private static class Node {
        int key, val;
        Node prev, next;
        Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    private final int capacity;
    private final HashMap<Integer, Node> map;
    private final Node head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.head = new Node(0, 0); // dummy head
        this.tail = new Node(0, 0); // dummy tail
        head.next = tail;
        tail.prev = head;
    }

    private void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void insertAtHead(Node node) {
        node.next = head.next;
        node.next.prev = node;
        head.next = node;
        node.prev = head;
    }

    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        remove(node);
        insertAtHead(node); // mark as most recently used
        return node.val;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.val = value;
            remove(node);
            insertAtHead(node);
        } else {
            if (map.size() == capacity) {
                Node lru = tail.prev; // evict least recently used node
                remove(lru);
                map.remove(lru.key);
            }
            Node newNode = new Node(key, value);
            insertAtHead(newNode);
            map.put(key, newNode);
        }
    }
}
class Node:
    def __init__(self, key: int, val: int):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.map = {}
        self.head = Node(0, 0)  # dummy head
        self.tail = Node(0, 0)  # dummy tail
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node: Node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _insert_at_head(self, node: Node):
        node.next = self.head.next
        node.next.prev = node
        self.head.next = node
        node.prev = self.head

    def get(self, key: int) -> int:
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._insert_at_head(node)
        return node.val

    def put(self, key: int, value: int) -> None:
        if key in self.map:
            node = self.map[key]
            node.val = value
            self._remove(node)
            self._insert_at_head(node)
        else:
            if len(self.map) == self.capacity:
                lru = self.tail.prev
                self._remove(lru)
                del self.map[lru.key]
            new_node = Node(key, value)
            self._insert_at_head(new_node)
            self.map[key] = new_node
#include <unordered_map>

class LRUCache {
private:
    struct Node {
        int key, val;
        Node* prev;
        Node* next;
        Node(int k, int v) : key(k), val(v), prev(nullptr), next(nullptr) {}
    };

    int capacity;
    std::unordered_map<int, Node*> map;
    Node* head;
    Node* tail;

    void remove(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void insertAtHead(Node* node) {
        node->next = head->next;
        node->next->prev = node;
        head->next = node;
        node->prev = head;
    }

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

    int get(int key) {
        if (map.find(key) == map.end()) return -1;
        Node* node = map[key];
        remove(node);
        insertAtHead(node);
        return node->val;
    }

    void put(int key, int value) {
        if (map.find(key) != map.end()) {
            Node* node = map[key];
            node->val = value;
            remove(node);
            insertAtHead(node);
        } else {
            if (map.size() == capacity) {
                Node* lru = tail->prev;
                remove(lru);
                map.erase(lru->key);
                delete lru;
            }
            Node* newNode = new Node(key, value);
            insertAtHead(newNode);
            map[key] = newNode;
        }
    }
};

Complexity Analysis

  • Time Complexity: O(1) for get and put since map lookups and list manipulations take constant time.
  • Space Complexity: O(capacity) auxiliary space to store elements up to the cache capacity limit.

Where It Breaks

If the cache is accessed concurrently by multiple threads, the doubly linked list pointers will experience race conditions and corrupt the cache structure. Synchronized blocks or lock striping are required to make it thread-safe in production.


Common Mistakes

  • Incorrect eviction key mapping: Removing the tail node from the list but forgetting to remove its corresponding entry from the hash map, leading to dangling key references.
  • Null pointer exceptions on boundaries: Forgetting dummy head and tail nodes, which requires writing complex conditional checks for empty lists.

← All Problems