Trung bình PHP

LRU Cache: Design an O(1) Cache with a Doubly Linked List and Hash Map

How to design an LRU cache that supports get and put in O(1) time using a hash map paired with a doubly linked list, with a working PHP implementation and a hands-on coding challenge.

28 Th08, 2026 4 phút 9 Lượt xem 1 Khối code
Sơ đồ
graph LR subgraph "Hash Map (key -> node pointer)" M1["'a' -> Node A"] M2["'b' -> Node B"] M3["'c' -> Node C"] end subgraph "Doubly Linked List (most to least recent)" HEAD((head)) <--> A["Node A key=a"] A <--> B["Node B key=b"] B <--> C["Node C key=c"] C <--> TAIL((tail)) end M1 -.-> A M2 -.-> B M3 -.-> C

An LRU (Least Recently Used) cache evicts the item that hasn't been touched in the longest time once it's full. It's a classic interview question, but it's also exactly what backs real systems: database query caches, CDN edge caches, browser tab memory management, connection pools.

The naive approach — an array you scan for the oldest entry — is O(n) per operation. The trick to O(1) is combining two structures: a hash map for O(1) lookup, and a doubly linked list for O(1) reordering.

The Structure

graph LR subgraph "Hash Map (key -> node pointer)" M1["'a' -> Node A"] M2["'b' -> Node B"] M3["'c' -> Node C"] end subgraph "Doubly Linked List (most to least recent)" HEAD((head)) <--> A["Node A key=a"] A <--> B["Node B key=b"] B <--> C["Node C key=c"] C <--> TAIL((tail)) end M1 -.-> A M2 -.-> B M3 -.-> C

Every node lives in the linked list AND has a pointer stored in the hash map. head is a sentinel pointing to the most recently used node; tail is a sentinel pointing to the least recently used one, which is the eviction candidate.

The Two Operations

get(key): hash map lookup finds the node in O(1). If found, unlink it from its current position and relink it right after head (it's now the most recently used), then return its value. If not found, return null/miss.

put(key, value): if the key already exists, update its value and move it to the front, same as get. If it's new and capacity is full, remove the node just before tail (the least recently used) from both the list and the hash map, then insert the new node at the front.

Both operations only ever touch a constant number of pointers — no scanning required.

PHP Implementation

class Node
{
    public function __construct(
        public string $key,
        public mixed $value,
        public ?Node $prev = null,
        public ?Node $next = null,
    ) {}
}

class LRUCache
{
    private array $map = [];
    private Node $head;
    private Node $tail;
    private int $capacity;

    public function __construct(int $capacity)
    {
        $this->capacity = $capacity;
        $this->head = new Node('', null);
        $this->tail = new Node('', null);
        $this->head->next = $this->tail;
        $this->tail->prev = $this->head;
    }

    public function get(string $key): mixed
    {
        if (!isset($this->map[$key])) {
            return null;
        }

        $node = $this->map[$key];
        $this->detach($node);
        $this->attachToFront($node);

        return $node->value;
    }

    public function put(string $key, mixed $value): void
    {
        if (isset($this->map[$key])) {
            $node = $this->map[$key];
            $node->value = $value;
            $this->detach($node);
            $this->attachToFront($node);
            return;
        }

        if (count($this->map) >= $this->capacity) {
            $lru = $this->tail->prev;
            $this->detach($lru);
            unset($this->map[$lru->key]);
        }

        $node = new Node($key, $value);
        $this->map[$key] = $node;
        $this->attachToFront($node);
    }

    private function detach(Node $node): void
    {
        $node->prev->next = $node->next;
        $node->next->prev = $node->prev;
    }

    private function attachToFront(Node $node): void
    {
        $node->next = $this->head->next;
        $node->prev = $this->head;
        $this->head->next->prev = $node;
        $this->head->next = $node;
    }
}

The two sentinel nodes (head and tail) exist purely to avoid null checks at the boundaries — every real node always has a valid prev and next to work with.

Common Pitfalls

Forgetting to update the hash map on eviction. If you remove a node from the linked list but leave its entry in $map, the next get() for that key returns a dangling node instead of a miss.

Off-by-one on capacity checks. Check count($this->map) >= $this->capacity before inserting the new node, not after — otherwise the cache temporarily holds capacity + 1 items.

Treating get() as read-only. A cache hit still mutates state — it moves the node to the front. Skipping that step turns your LRU into a plain FIFO cache.

Using an array and array_shift() as the "queue." array_shift() is O(n) because PHP reindexes the array. This defeats the entire point of the exercise — the doubly linked list is what makes eviction O(1).

When to Reach for a Real Implementation

Don't hand-roll this for production unless you have a specific reason (embedding the cache inside a single PHP process with no external dependency, for instance). For anything shared across requests or processes, Redis's own eviction policies (allkeys-lru, volatile-lru) or a library like symfony/cache already solve this correctly and durably. The value of building it yourself is understanding why O(1) requires both structures together — neither one alone gets you there.

Thử thách tương tác

Thử thách

Luyện tập ngay điều vừa học. Viết lời giải, mở gợi ý nếu bí.

Đề bài

Implement the LRUCache class below. It must support get(key) and put(key, value), each running in O(1) time, using a fixed capacity passed to the constructor. When put() is called on a full cache with a new key, evict the least recently used entry before inserting. Both get() and put() on an existing key should mark that key as most recently used. Fill in the TODOs in the Node and LRUCache classes.

Ngôn ngữPHP

Code khởi tạo

php
class Node
{
    public function __construct(
        public string $key,
        public mixed $value,
        public ?Node $prev = null,
        public ?Node $next = null,
    ) {}
}

class LRUCache
{
    private array $map = [];
    private Node $head;
    private Node $tail;
    private int $capacity;

    public function __construct(int $capacity)
    {
        $this->capacity = $capacity;
        // TODO: initialize $head and $tail sentinel nodes and link them together
    }

    public function get(string $key): mixed
    {
        // TODO: return null on miss; on hit, move node to front and return its value
    }

    public function put(string $key, mixed $value): void
    {
        // TODO: update+move if key exists; otherwise evict LRU if full, then insert at front
    }

    private function detach(Node $node): void
    {
        // TODO: unlink $node from its neighbors
    }

    private function attachToFront(Node $node): void
    {
        // TODO: insert $node right after $head
    }
}

Lời giải của bạn

Gợi ý

Đã mở hết gợi ý
Gợi ý 1

Use two sentinel nodes for head and tail to avoid null checks

Lời giải · php
class Node
{
    public function __construct(
        public string $key,
        public mixed $value,
        public ?Node $prev = null,
        public ?Node $next = null,
    ) {}
}

class LRUCache
{
    private array $map = [];
    private Node $head;
    private Node $tail;
    private int $capacity;

    public function __construct(int $capacity)
    {
        $this->capacity = $capacity;
        $this->head = new Node('', null);
        $this->tail = new Node('', null);
        $this->head->next = $this->tail;
        $this->tail->prev = $this->head;
    }

    public function get(string $key): mixed
    {
        if (!isset($this->map[$key])) {
            return null;
        }

        $node = $this->map[$key];
        $this->detach($node);
        $this->attachToFront($node);

        return $node->value;
    }

    public function put(string $key, mixed $value): void
    {
        if (isset($this->map[$key])) {
            $node = $this->map[$key];
            $node->value = $value;
            $this->detach($node);
            $this->attachToFront($node);
            return;
        }

        if (count($this->map) >= $this->capacity) {
            $lru = $this->tail->prev;
            $this->detach($lru);
            unset($this->map[$lru->key]);
        }

        $node = new Node($key, $value);
        $this->map[$key] = $node;
        $this->attachToFront($node);
    }

    private function detach(Node $node): void
    {
        $node->prev->next = $node->next;
        $node->next->prev = $node->prev;
    }

    private function attachToFront(Node $node): void
    {
        $node->next = $this->head->next;
        $node->prev = $this->head;
        $this->head->next->prev = $node;
        $this->head->next = $node;
    }
}

Cùng chủ đề PHP