Learnitweb

LRU Cache

Problem Statement

Design a Least Recently Used (LRU) Cache data structure.

You need to implement a class LRUCache with the following operations:

  1. get(int key) – Return the value associated with the key if it exists, otherwise return -1.
  2. 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:

  1. Retrieve a value quickly using a key (like a HashMap).
  2. 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:

  1. HashMap – to store key → node mapping for O(1) access.
  2. 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:

  1. addNode(node) – Add a new node right after the head.
  2. removeNode(node) – Disconnect a node from the list.
  3. moveToHead(node) – Move an existing node to the head.
  4. popTail() – Remove the least recently used node (the one before tail).

Step-by-Step Plan

  1. Create a Node class with fields: key, value, prev, and next.
  2. Use a HashMap<Integer, Node> for quick access.
  3. Use a doubly linked list with dummy head and tail.
  4. On every get(key):
    • If key not found, return -1.
    • If found, move that node to the head (mark it most recently used).
  5. 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

  1. Initialization
    • The cache capacity is set in the constructor.
    • Dummy head and tail nodes mark the boundaries.
  2. 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.
  3. 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.
  4. 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.