LRU Cache: Optimal O(1) Solution (Data-Backed Guide)

By DSA Prep Team · March 4, 2026 · 10 min read · Algorithms

LRU Cache is the #6 most asked LeetCode problem globally — and one of the most important system design + DSA crossover problems you'll face.

Our analysis of 10,385 verified interview questions across 259 companies shows LRU Cache appeared 116 times across 50 companies — including Google, Amazon, Meta, Microsoft, Bloomberg, Goldman Sachs, Citadel, Cloudflare, DoorDash, LinkedIn, and MongoDB.

This problem is special: it tests your ability to design a data structure from scratch, not just apply an existing algorithm. The solution requires combining a HashMap (for O(1) lookup) with a Doubly Linked List (for O(1) insertion and deletion). Get this right and you've demonstrated real engineering intuition.

Table of Contents

The Data: Why 50 Companies Ask This

MetricValue
Total Interview Appearances116 verified mentions
Companies That Ask It50 companies
Global Rank#6 out of 10,385 questions
DifficultyMedium
Core PatternsDesign, Hash Table, Doubly Linked List

📌 Companies That Ask This Problem

Google, Amazon, Meta, Microsoft, Bloomberg, Goldman Sachs, Citadel, Cloudflare, DoorDash, LinkedIn, MongoDB, Confluent, Cisco, Capital One, ByteDance, Expedia, Grab, IBM, Intel, Intuit, J.P. Morgan, Morgan Stanley, Disney, Adobe, Autodesk, Apple, Booking.com, Cruise, and 22 more.

LRU Cache is asked by infrastructure, systems, and product companies alike because caches are everywhere in real engineering — CDNs, database query caches, OS page replacement, CPU caches. Knowing how to implement one signals you understand both systems thinking and data structure design.

Problem Statement

LeetCode #146 — LRU Cache

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

Implement the LRUCache class:

Both get and put must run in O(1) average time complexity.

Example
LRUCache cache = new LRUCache(2);  // capacity = 2
cache.put(1, 1);   // cache: {1=1}
cache.put(2, 2);   // cache: {1=1, 2=2}
cache.get(1);      // returns 1, cache: {2=2, 1=1} (1 now most recent)
cache.put(3, 3);   // evicts key 2, cache: {1=1, 3=3}
cache.get(2);      // returns -1 (not found)
cache.put(4, 4);   // evicts key 1, cache: {3=3, 4=4}
cache.get(1);      // returns -1
cache.get(3);      // returns 3
cache.get(4);      // returns 4

The Core Intuition: Why HashMap + DLL?

The challenge: we need O(1) for three operations — lookup by key, move-to-front on access, and delete the tail (LRU) on eviction.

💡 Why Doubly Linked (not Singly)?

To remove a node from the middle of a linked list in O(1), you need a pointer to the previous node. A singly linked list requires O(n) traversal to find the predecessor. A doubly linked list stores prev and next on every node, making removal O(1) given the node pointer.

The Algorithm: Design Walkthrough

Use two sentinel nodes — head (dummy most-recent) and tail (dummy least-recent). Real nodes sit between them.

Helper methods: remove(node) — unlinks a node from the list. insert_after_head(node) — places a node right after the head sentinel.

Code in Python, Java & JavaScript

Python 3
class Node:
    def __init__(self, key=0, val=0):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.map = {}  # key -> node

        # Sentinel head (most recent) and tail (least recent)
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head

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

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

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

    def put(self, key: int, value: int) -> None:
        if key in self.map:
            self._remove(self.map[key])
        node = Node(key, value)
        self.map[key] = node
        self._insert_after_head(node)

        if len(self.map) > self.capacity:
            lru = self.tail.prev  # node just before tail = LRU
            self._remove(lru)
            del self.map[lru.key]
Java
class LRUCache {
    private class Node {
        int key, val;
        Node prev, next;
        Node(int k, int v) { key = k; val = v; }
    }

    private int capacity;
    private Map<Integer, Node> map = new HashMap<>();
    private Node head = new Node(0, 0); // most recent sentinel
    private Node tail = new Node(0, 0); // least recent sentinel

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }

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

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

    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        remove(node);
        insertAfterHead(node);
        return node.val;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) remove(map.get(key));
        Node node = new Node(key, value);
        map.put(key, node);
        insertAfterHead(node);

        if (map.size() > capacity) {
            Node lru = tail.prev;
            remove(lru);
            map.remove(lru.key);
        }
    }
}
JavaScript
class Node {
    constructor(key = 0, val = 0) {
        this.key = key; this.val = val;
        this.prev = null; this.next = null;
    }
}

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.map = new Map();
        this.head = new Node(); // most recent sentinel
        this.tail = new Node(); // least recent sentinel
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    _remove(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    _insertAfterHead(node) {
        node.next = this.head.next;
        node.prev = this.head;
        this.head.next.prev = node;
        this.head.next = node;
    }

    get(key) {
        if (!this.map.has(key)) return -1;
        const node = this.map.get(key);
        this._remove(node);
        this._insertAfterHead(node);
        return node.val;
    }

    put(key, value) {
        if (this.map.has(key)) this._remove(this.map.get(key));
        const node = new Node(key, value);
        this.map.set(key, node);
        this._insertAfterHead(node);

        if (this.map.size > this.capacity) {
            const lru = this.tail.prev;
            this._remove(lru);
            this.map.delete(lru.key);
        }
    }
}

Complexity Analysis

📊 Complexity Summary

How to Actually Remember This

✅ Your Spaced Repetition Schedule

  1. Today: Implement LC #146 from scratch. Draw the DLL with head/tail sentinels before coding.
  2. Day 3: Implement LFU Cache (LC #460) — same idea but track frequency instead of recency.
  3. Day 7: Re-implement without looking at notes. Focus on the _remove and _insert_after_head helpers.
  4. Day 14: Explain the design to someone. If you can teach it, you own it.

Tag this on DSAPrep.dev under Design and Linked List.

Track on DSAPrep.dev →