Skip to content
AI360Xpert
Back to System-Design-Adjacent / OOP Coding
Hard

LFU Cache

Design and implement a data structure for a Least Frequently Used (LFU) cache. Implement the `LFUCache` class: - `LFUCache(int capacity)` Initializes the object with the `capacity` of the data structure. - `int get(int key)` Gets the value of the `key` if the `key` exists in the cache. Otherwise, returns `-1`. - `void put(int key, int value)` Update the value of the `key` if present, or inserts the `key` if not already present. When the cache reaches its `capacity`, it should invalidate and remove the **least frequently used** key before inserting a new item. For this problem, when there is a **tie** (i.e., two or more keys with the same frequency), the **least recently used** key would be invalidated. To determine the least frequently used key, a **use counter** is maintained for each key in the cache. The key with the smallest use counter is the least frequently used key. When a key is first inserted into the cache, its use counter is set to `1` (due to the `put` operation). The use counter for a key in the cache is incremented either a `get` or `put` operation is called on it. The functions `get` and `put` must each run in `O(1)` average time complexity.

Examples

Input:["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
Output:[null, null, null, 1, null, -1, 3, null, -1, 3, 4]
// cnt(x) = the use counter for key x // cache=[] will show the last used order for tiebreakers (leftmost element is most recent) LFUCache lfu = new LFUCache(2); lfu.put(1, 1); // cache=[1,_], cnt(1)=1 lfu.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1 lfu.get(1); // return 1 // cache=[1,2], cnt(1)=2, cnt(2)=1 lfu.put(3, 3); // 2 is the LFU key because cnt(2)=1 is the minimum, hit LRU rule. cache=[3,1], cnt(3)=1, cnt(1)=2 lfu.get(2); // return -1 (not found) lfu.get(3); // return 3 // cache=[3,1], cnt(3)=2, cnt(1)=2 lfu.put(4, 4); // Both 1 and 3 have the same cnt, but 1 is LRU, invalidate 1. // cache=[4,3], cnt(4)=1, cnt(3)=2 lfu.get(1); // return -1 (not found) lfu.get(3); // return 3 // cache=[3,4], cnt(3)=3, cnt(4)=1 lfu.get(4); // return 4 // cache=[4,3], cnt(4)=2, cnt(3)=3

Constraints

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

Optimal Solution

Approach

To achieve O(1) time complexity for both `get` and `put` operations, we can combine a hash map with doubly linked lists. 1. **Data Structures**: * **Node**: Stores the `key`, `value`, and its `frequency`. It also has `prev` and `next` pointers to be used in a doubly linked list. * **Doubly Linked List (DLL)**: For a specific frequency, we maintain a DLL of nodes. The head represents the most recently used (MRU) node, and the tail represents the least recently used (LRU) node. * **Key Map (`key_to_node`)**: A hash map mapping a `key` to its corresponding `Node`. This allows O(1) access to any node. * **Frequency Map (`freq_to_dll`)**: A hash map mapping a `frequency` to a `Doubly Linked List`. This keeps track of all nodes having the same frequency, ordered by recency. * **min_freq**: An integer tracking the minimum frequency among all nodes currently in the cache. 2. **Get Operation**: * If the `key` is not in `key_to_node`, return -1. * Otherwise, fetch the node. We need to update its frequency: * Remove the node from the DLL in `freq_to_dll` corresponding to its current frequency. * If this DLL becomes empty and the node's frequency was equal to `min_freq`, increment `min_freq`. * Increment the node's frequency. * Add the node to the head of the DLL in `freq_to_dll` corresponding to its new frequency. * Return the node's value. 3. **Put Operation**: * If `capacity` is 0, do nothing. * If the `key` already exists, update its value and perform the same frequency update logic as in the `get` operation. * If the `key` is new: * If the cache is at full capacity, we need to evict the LFU/LRU node: * Get the DLL corresponding to `min_freq` from `freq_to_dll`. * Remove the LRU node (which is at the tail of this DLL). * Remove the corresponding key from `key_to_node`. * Create a new node with frequency 1. * Add it to `key_to_node`. * Add it to the head of the DLL for frequency 1 in `freq_to_dll`. * Reset `min_freq` to 1.

Complexity Analysis

Time Complexity
O(1)
Space Complexity
O(N)
Solution.java
import java.util.HashMap;import java.util.Map;
class LFUCache {    class Node {        int key, val, freq;        Node prev, next;        Node(int key, int val) {            this.key = key;            this.val = val;            this.freq = 1;        }    }        class DoublyLinkedList {        Node head, tail;        int size;                DoublyLinkedList() {            head = new Node(0, 0);            tail = new Node(0, 0);            head.next = tail;            tail.prev = head;            size = 0;        }                void addNode(Node node) {            node.next = head.next;            node.prev = head;            head.next.prev = node;            head.next = node;            size++;        }                void removeNode(Node node) {            node.prev.next = node.next;            node.next.prev = node.prev;            size--;        }                Node popTail() {            if (size == 0) return null;            Node res = tail.prev;            removeNode(res);            return res;        }    }        int capacity, size, minFreq;    Map<Integer, Node> keyToNode;    Map<Integer, DoublyLinkedList> freqToDll;
    public LFUCache(int capacity) {        this.capacity = capacity;        this.size = 0;        this.minFreq = 0;        this.keyToNode = new HashMap<>();        this.freqToDll = new HashMap<>();    }        private void update(Node node) {        int freq = node.freq;        DoublyLinkedList dll = freqToDll.get(freq);        dll.removeNode(node);        if (minFreq == freq && dll.size == 0) {            minFreq++;        }        node.freq++;        freqToDll.computeIfAbsent(node.freq, k -> new DoublyLinkedList()).addNode(node);    }        public int get(int key) {        if (!keyToNode.containsKey(key)) {            return -1;        }        Node node = keyToNode.get(key);        update(node);        return node.val;    }        public void put(int key, int value) {        if (capacity == 0) return;                if (keyToNode.containsKey(key)) {            Node node = keyToNode.get(key);            node.val = value;            update(node);        } else {            if (size == capacity) {                DoublyLinkedList minDll = freqToDll.get(minFreq);                Node lruNode = minDll.popTail();                keyToNode.remove(lruNode.key);                size--;            }            Node newNode = new Node(key, value);            keyToNode.put(key, newNode);            freqToDll.computeIfAbsent(1, k -> new DoublyLinkedList()).addNode(newNode);            minFreq = 1;            size++;        }    }}