Problem Statement
Design a Least Recently Used (LRU) Cache data structure.
You need to implement a class LRUCache with the following operations:
- get(int key) – Return the value associated with the key if it exists, otherwise return -1.
- put(int key, int value) – Insert or update the value of the key.
- If the cache reaches its capacity, remove the least recently used (LRU) item before inserting a new one.
Example
Input: LRUCache cache = new LRUCache(2); // capacity = 2 cache.put(1, 1); cache.put(2, 2); cache.get(1); // returns 1 cache.put(3, 3); // removes key 2 (least recently used) cache.get(2); // returns -1 (not found) cache.put(4, 4); // removes key 1 cache.get(1); // returns -1 (not found) cache.get(3); // returns 3 cache.get(4); // returns 4
Output: [1, -1, -1, 3, 4]
Understanding the Problem
An LRU Cache should do two things efficiently:
- Retrieve a value quickly using a key (like a HashMap).
- Track usage order — know which key was used most recently and which was used least recently.
If the cache is full, we must remove the least recently used key before inserting a new one.
We need both:
- Fast access (
O(1)) - Fast removal/insertion of least recently used element
A normal HashMap gives O(1) lookup but cannot track the order of usage efficiently.
A LinkedList can track order but does not give O(1) lookup.
Hence, we combine the two.
Thinking About the Approach
Main Idea:
Use two data structures together:
- HashMap – to store key → node mapping for O(1) access.
- Doubly Linked List – to maintain the order of usage.
We keep:
- The most recently used node near the head.
- The least recently used node near the tail.
Whenever:
- We access (get) a node → move it to the head.
- We insert (put) a new node → add it to the head.
- If capacity is full → remove the node at the tail.
Key Operations in the Doubly Linked List
We use a dummy head and dummy tail to simplify logic:
- Head’s next node is the most recently used.
- Tail’s previous node is the least recently used.
Operations:
- addNode(node) – Add a new node right after the head.
- removeNode(node) – Disconnect a node from the list.
- moveToHead(node) – Move an existing node to the head.
- popTail() – Remove the least recently used node (the one before tail).
Step-by-Step Plan
- Create a
Nodeclass with fields:key,value,prev, andnext. - Use a
HashMap<Integer, Node>for quick access. - Use a doubly linked list with dummy head and tail.
- On every get(key):
- If key not found, return -1.
- If found, move that node to the head (mark it most recently used).
- On every put(key, value):
- If key exists, update its value and move it to head.
- If key does not exist:
- Create a new node and add it to head.
- Add it to the map.
- If capacity exceeded, remove the least recently used node (tail’s previous).
Java Implementation
import java.util.HashMap;
class LRUCache {
class Node {
int key, value;
Node prev, next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
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);
this.tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = map.get(key);
if (node == null)
return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
} else {
Node newNode = new Node(key, value);
map.put(key, newNode);
addNode(newNode);
if (map.size() > capacity) {
Node tailNode = popTail();
map.remove(tailNode.key);
}
}
}
private void addNode(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node node) {
Node prev = node.prev;
Node next = node.next;
prev.next = next;
next.prev = prev;
}
private void moveToHead(Node node) {
removeNode(node);
addNode(node);
}
private Node popTail() {
Node res = tail.prev;
removeNode(res);
return res;
}
}
Explanation of Code
- Initialization
- The cache capacity is set in the constructor.
- Dummy
headandtailnodes mark the boundaries.
- get(key)
- Look up the key in the map.
- If found, move that node to the head (most recently used).
- Return its value, or -1 if not found.
- put(key, value)
- If the key already exists, update the value and move it to the head.
- If the key is new:
- Add it as a new node right after the head.
- If capacity exceeds, remove the tail’s previous node.
- addNode(), removeNode(), moveToHead(), popTail()
- These are helper methods to manage the linked list efficiently.
Example Walkthrough
Capacity = 2
Step 1: put(1, 1)
Cache = {1=1} → Most recent: 1
Step 2: put(2, 2)
Cache = {1=1, 2=2} → Most recent: 2
Step 3: get(1) → returns 1
Now 1 becomes most recent.
Cache order (from most recent): 1, 2
Step 4: put(3, 3)
Cache is full → remove least recent (2).
Add 3.
Cache = {1=1, 3=3} → Most recent: 3
Step 5: get(2) → not found → return -1
Step 6: get(3) → return 3
Complexity Analysis
Time Complexity:
O(1) for both get() and put() operations, because all list and map operations are constant time.
Space Complexity:
O(capacity) for storing up to capacity elements in the map and linked list.
Summary
- The LRU Cache problem tests understanding of combining HashMap and Doubly Linked List.
- HashMap gives constant-time access.
- Doubly Linked List maintains the order of use.
- On every access or insertion, we move the item to the head.
- When the cache is full, we remove the item at the tail.
This combination ensures both speed and order tracking are handled efficiently.
