Jump to section

An LRU (least recently used) cache stores a fixed number of key-value pairs and, when full, evicts the entry that was used longest ago. The interview answer pairs a hash map with a doubly linked list, so get and put both run in O(1) time, with O(capacity) space.

The Problem

Design a cache with a fixed capacity and two operations. get(key) returns the value if the key is present, otherwise -1. put(key, value) inserts or updates the key. Both operations count as "using" the key. When a put would take the cache over capacity, remove the least recently used key first. Both operations must run in constant time on average.

Example 1

Capacity 2. put(1, 10), put(2, 20), get(1) returns 10 and makes key 1 the most recent. put(3, 30) now evicts key 2, so get(2) returns -1.

Example 2

Capacity 1. put(1, 1), then put(2, 2) evicts key 1 immediately. get(1) returns -1 and get(2) returns 2.

Assumptions to confirm

  • Does updating an existing key count as a use? Normally yes.
  • Is capacity always at least 1?
  • May you use your language's ordered map, or does the interviewer want the linked list built by hand? Ask. Many expect you to build it.

The Naive Approach

A plain hash map gives O(1) lookups but no idea of order. Adding a timestamp to each entry and scanning for the oldest on eviction makes put O(n). Keeping keys in an array or list ordered by use makes moving a key to the front O(n). Say this out loud: the problem is that you need fast lookup and fast reordering at the same time, and no single basic structure gives you both.

Ready to put this into practice?

Practice this with MockIF →

Still applying for jobs? Let Auto Apply do it

The Optimal Design: Hash Map Plus Doubly Linked List

1

Keep usage order in a doubly linked list

The most recently used node sits at one end and the least recently used at the other. Removing a node or adding one at the end is O(1) when you hold a pointer to it.

2

Map each key to its node

The hash map gives O(1) access to the node, so you never search the list.

3

Use two sentinel nodes

A dummy head and tail mean you never check for null neighbours, which removes most of the bugs in this question.

4

On get, move the node to the recent end

Unlink it and append it before the tail sentinel, then return its value.

5

On put, insert or update, then evict if over capacity

The node after the head sentinel is the least recently used. Unlink it and delete its key from the map. Store the key on the node so this delete is possible.

Python and TypeScript Solutions

Python, with OrderedDict

OrderedDict is a hash map that remembers order and can move a key to the end in O(1), so it is the shortest correct answer.

from collections import OrderedDict


class LRUCache:
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self.items: OrderedDict[int, int] = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.items:
            return -1
        self.items.move_to_end(key)          # now the most recently used
        return self.items[key]

    def put(self, key: int, value: int) -> None:
        if key in self.items:
            self.items.move_to_end(key)
        self.items[key] = value
        if len(self.items) > self.capacity:
            self.items.popitem(last=False)   # evict the least recently used

Python, built by hand

If the interviewer rules out OrderedDict, this is the hash map plus doubly linked list version.

class Node:
    def __init__(self, key: int = 0, value: int = 0) -> None:
        self.key, self.value = key, value
        self.prev: "Node | None" = None
        self.next: "Node | None" = None


class LRUCacheManual:
    """Hash map + doubly linked list, for when the interviewer says 'no OrderedDict'."""

    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self.nodes: dict[int, Node] = {}
        self.head, self.tail = Node(), Node()   # sentinels: head.next is the LRU item
        self.head.next, self.tail.prev = self.tail, self.head

    def _unlink(self, node: Node) -> None:
        node.prev.next, node.next.prev = node.next, node.prev

    def _append(self, node: Node) -> None:      # insert just before tail (most recent)
        node.prev, node.next = self.tail.prev, self.tail
        self.tail.prev.next = node
        self.tail.prev = node

    def get(self, key: int) -> int:
        node = self.nodes.get(key)
        if node is None:
            return -1
        self._unlink(node)
        self._append(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        if key in self.nodes:
            self._unlink(self.nodes[key])
        node = Node(key, value)
        self.nodes[key] = node
        self._append(node)
        if len(self.nodes) > self.capacity:
            lru = self.head.next
            self._unlink(lru)
            del self.nodes[lru.key]

TypeScript

A JavaScript Map iterates keys in insertion order, so deleting and re-inserting a key moves it to the most recent end, and the first key is always the least recently used.

class LRUCache {
  private items = new Map<number, number>();
  private capacity: number;

  constructor(capacity: number) {
    this.capacity = capacity;
  }

  get(key: number): number {
    if (!this.items.has(key)) return -1;
    const value = this.items.get(key)!;
    this.items.delete(key); // re-insert so it becomes the most recently used
    this.items.set(key, value);
    return value;
  }

  put(key: number, value: number): void {
    this.items.delete(key);
    this.items.set(key, value);
    if (this.items.size > this.capacity) {
      // A Map iterates in insertion order, so the first key is the least recently used.
      const lru = this.items.keys().next().value as number;
      this.items.delete(lru);
    }
  }
}

Ready to put this into practice?

Practice this with MockIF →

Still applying for jobs? Let Auto Apply do it

Complexity Compared

ApproachgetputSpace
Hash map with timestampsO(1)O(n) to find the oldestO(capacity)
Array kept in usage orderO(n)O(n)O(capacity)
Hash map plus doubly linked listO(1)O(1)O(capacity)
Ordered map (OrderedDict or Map)O(1)O(1)O(capacity)

How to Talk Through It in an Interview

  1. Clarify: "Both get and put count as use? Capacity is at least 1? Can I use an ordered map, or should I build the list?"
  2. Naive: "A hash map alone can't tell me which key is oldest without a scan, and a list alone can't find a key without a scan."
  3. Design: "So I combine them: the map finds the node, the doubly linked list keeps usage order, and both operations are O(1)."
  4. Code: write the two helpers first, unlink and append, then get and put on top of them. Mention the sentinels and why each node stores its key.
  5. Test: walk through the capacity 2 example, then capacity 1, then updating an existing key.

Ready to put this into practice?

Practice this with MockIF →

Still applying for jobs? Let Auto Apply do it

Edge Cases and Follow-Ups

Updating an existing key

Move it to the recent end and change its value. The size does not grow, so nothing is evicted.

Capacity of one

Every new key evicts the previous one. It is a quick way to catch off-by-one errors in eviction.

Follow-up: make it thread-safe

Wrap get and put in a lock, since both change the order. Mention that finer-grained locking or sharding helps under heavy load.

Follow-up: LFU cache

Least frequently used needs a frequency count per key and a list of keys per frequency. It is a common harder follow-up.

Follow-up: add expiry times

Store an expiry on each node and treat expired entries as missing on get. Clean-up can be lazy or done by a background pass.

Practise It in a Mock Coding Round

Reading a solution is not the same as producing it with someone watching. In a real round you have to clarify the problem, say the brute force out loud, and keep talking while you type. MockIF runs coding rounds with a voice AI interviewer in a real in-browser editor, in Python and TypeScript. It runs your code against tests, asks you to explain your approach, follows up on edge cases and complexity, then scores the round and gives you specific feedback. A session costs 2 credits. It is free to start with 5 credits, then monthly or 3-monthly plans, or one-off credit packs (see pricing). Read more about how a mock coding interview works.

Frequently Asked Questions

What data structure is used for an LRU cache?
A hash map combined with a doubly linked list. The map finds a key's node in O(1), and the list keeps nodes in usage order so moving and evicting are also O(1).
Why a doubly linked list and not a singly linked one?
To remove a node in O(1) you need its previous node. A doubly linked list stores that pointer; a singly linked list would need a scan to find it.
Can I use OrderedDict in a Python interview?
Often yes, and it is the shortest correct answer. Ask first, and be ready to build the linked list version if the interviewer wants to see the design.
Is LRU cache a design question or a coding question?
Both. It is asked as a coding problem, but the interviewer is mostly assessing how you combine data structures to hit O(1) for every operation.
What is the difference between LRU and LFU?
LRU evicts the entry used longest ago. LFU evicts the entry used least often, breaking ties by age. LFU needs extra bookkeeping for frequencies.

Stop Preparing in Your Head. Start Practicing Out Loud.

Drop your resume, add a job description, and get a mock interview like the real thing.

Start Free Mock Interview →
No credit card required. 5 free credits to start.

Still applying for jobs? Let Auto Apply do it for you.

Try Auto Apply →