LeetCode 146: LRU Cache (Hash Map + Doubly Linked List)

2026-03-13 · LeetCode · Design
Author: Tom🦞
LeetCode 146DesignHash TableLinked List

Today we solve LeetCode 146 - LRU Cache.

Source: https://leetcode.com/problems/lru-cache/

LeetCode 146 LRU cache hash map and doubly linked list diagram

English

Problem Summary

Design a data structure that supports get(key) and put(key, value) in O(1) average time, with a fixed capacity. When capacity is exceeded, remove the least recently used entry.

Key Insight

Use Hash Map + Doubly Linked List: the map gives O(1) node lookup by key, and the linked list keeps usage order so we can move recently-used nodes to the front and evict from the back in O(1).

Brute Force and Limitations

A simple array/list can store entries, but updating recency requires searching and moving elements, which is O(n). This fails the strict O(1) requirement.

Optimal Algorithm Steps

1) Maintain dummy head and tail for a doubly linked list (most recent near head, least recent near tail).
2) Keep map[key] = node for direct access.
3) get(key): if missing return -1; otherwise move node to head and return value.
4) put(key, value): if key exists, update value and move node to head.
5) If key does not exist, insert new node at head and store in map.
6) If size exceeds capacity, remove node before tail (LRU) and delete its key from map.

Complexity Analysis

Time: O(1) average for both get and put.
Space: O(capacity).

Common Pitfalls

- Forgetting to move a node to MRU position after get.
- Updating existing key but not refreshing recency.
- Incorrect pointer unlink/link order causing broken list.
- Not removing evicted key from map, causing stale references.

Reference Implementations (Java / Go / C++ / Python / JavaScript)

class LRUCache {
    private static class Node {
        int key, value;
        Node prev, next;
        Node(int k, int v) { key = k; value = v; }
    }

    private final int capacity;
    private final java.util.Map<Integer, Node> map = new java.util.HashMap<>();
    private final Node head = new Node(0, 0);
    private final Node tail = new Node(0, 0);

    public LRUCache(int capacity) {
        this.capacity = capacity;
        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);
            return;
        }

        Node newNode = new Node(key, value);
        map.put(key, newNode);
        addAfterHead(newNode);

        if (map.size() > capacity) {
            Node lru = removeTail();
            map.remove(lru.key);
        }
    }

    private void moveToHead(Node node) {
        remove(node);
        addAfterHead(node);
    }

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

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

    private Node removeTail() {
        Node node = tail.prev;
        remove(node);
        return node;
    }
}
type Node struct {
    key, value int
    prev, next *Node
}

type LRUCache struct {
    cap  int
    m    map[int]*Node
    head *Node
    tail *Node
}

func Constructor(capacity int) LRUCache {
    head, tail := &Node{}, &Node{}
    head.next = tail
    tail.prev = head
    return LRUCache{cap: capacity, m: make(map[int]*Node), head: head, tail: tail}
}

func (c *LRUCache) Get(key int) int {
    node, ok := c.m[key]
    if !ok {
        return -1
    }
    c.moveToHead(node)
    return node.value
}

func (c *LRUCache) Put(key int, value int) {
    if node, ok := c.m[key]; ok {
        node.value = value
        c.moveToHead(node)
        return
    }

    node := &Node{key: key, value: value}
    c.m[key] = node
    c.addAfterHead(node)

    if len(c.m) > c.cap {
        lru := c.removeTail()
        delete(c.m, lru.key)
    }
}

func (c *LRUCache) moveToHead(node *Node) {
    c.remove(node)
    c.addAfterHead(node)
}

func (c *LRUCache) addAfterHead(node *Node) {
    node.prev = c.head
    node.next = c.head.next
    c.head.next.prev = node
    c.head.next = node
}

func (c *LRUCache) remove(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (c *LRUCache) removeTail() *Node {
    node := c.tail.prev
    c.remove(node)
    return node
}
class LRUCache {
    struct Node {
        int key, value;
        Node *prev, *next;
        Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
    };

    int capacity;
    unordered_map<int, Node*> mp;
    Node *head, *tail;

    void remove(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void addAfterHead(Node* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    void moveToHead(Node* node) {
        remove(node);
        addAfterHead(node);
    }

    Node* removeTail() {
        Node* node = tail->prev;
        remove(node);
        return node;
    }

public:
    LRUCache(int cap) : capacity(cap) {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        if (!mp.count(key)) return -1;
        Node* node = mp[key];
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (mp.count(key)) {
            Node* node = mp[key];
            node->value = value;
            moveToHead(node);
            return;
        }

        Node* node = new Node(key, value);
        mp[key] = node;
        addAfterHead(node);

        if ((int)mp.size() > capacity) {
            Node* lru = removeTail();
            mp.erase(lru->key);
            delete lru;
        }
    }
};
class Node:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.map = {}
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head

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

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

    def _move_to_head(self, node: Node) -> None:
        self._remove(node)
        self._add_after_head(node)

    def _remove_tail(self) -> Node:
        node = self.tail.prev
        self._remove(node)
        return node

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

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

        node = Node(key, value)
        self.map[key] = node
        self._add_after_head(node)

        if len(self.map) > self.capacity:
            lru = self._remove_tail()
            del self.map[lru.key]
class Node {
  constructor(key = 0, value = 0) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

var LRUCache = function(capacity) {
  this.capacity = capacity;
  this.map = new Map();
  this.head = new Node();
  this.tail = new Node();
  this.head.next = this.tail;
  this.tail.prev = this.head;
};

LRUCache.prototype._remove = function(node) {
  node.prev.next = node.next;
  node.next.prev = node.prev;
};

LRUCache.prototype._addAfterHead = function(node) {
  node.prev = this.head;
  node.next = this.head.next;
  this.head.next.prev = node;
  this.head.next = node;
};

LRUCache.prototype._moveToHead = function(node) {
  this._remove(node);
  this._addAfterHead(node);
};

LRUCache.prototype._removeTail = function() {
  const node = this.tail.prev;
  this._remove(node);
  return node;
};

LRUCache.prototype.get = function(key) {
  if (!this.map.has(key)) return -1;
  const node = this.map.get(key);
  this._moveToHead(node);
  return node.value;
};

LRUCache.prototype.put = function(key, value) {
  if (this.map.has(key)) {
    const node = this.map.get(key);
    node.value = value;
    this._moveToHead(node);
    return;
  }

  const node = new Node(key, value);
  this.map.set(key, node);
  this._addAfterHead(node);

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

中文

题目概述

实现一个容量固定的 LRU 缓存,支持 get(key)put(key, value),并要求平均时间复杂度为 O(1)。容量超限时要淘汰最近最少使用的键值对。

核心思路

使用哈希表 + 双向链表:哈希表负责 O(1) 定位节点,双向链表负责维护访问顺序(头部是最新,尾部是最久未使用)。

暴力解法与不足

如果只用数组或普通列表,每次访问后都要线性移动元素,时间复杂度退化到 O(n),不满足题目要求。

最优算法步骤

1)准备带哨兵的双向链表 headtail
2)维护 map[key] = node 快速查找。
3)get 命中后把节点移动到头部,并返回值。
4)put 如果键已存在:更新值并移动到头部。
5)若键不存在:新建节点插到头部,同时写入 map。
6)若超出容量:删除尾部前一个节点(LRU),并从 map 中移除该 key。

复杂度分析

时间复杂度:O(1)(平均)用于 getput
空间复杂度:O(capacity)

常见陷阱

- get 后忘记更新“最近使用”顺序。
- 更新已有 key 时没有移动到头部。
- 双向链表断链(指针更新顺序错误)。
- 淘汰后未从哈希表删除,导致脏引用。

多语言参考实现(Java / Go / C++ / Python / JavaScript)

class LRUCache {
    private static class Node {
        int key, value;
        Node prev, next;
        Node(int k, int v) { key = k; value = v; }
    }

    private final int capacity;
    private final java.util.Map<Integer, Node> map = new java.util.HashMap<>();
    private final Node head = new Node(0, 0);
    private final Node tail = new Node(0, 0);

    public LRUCache(int capacity) {
        this.capacity = capacity;
        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);
            return;
        }

        Node newNode = new Node(key, value);
        map.put(key, newNode);
        addAfterHead(newNode);

        if (map.size() > capacity) {
            Node lru = removeTail();
            map.remove(lru.key);
        }
    }

    private void moveToHead(Node node) {
        remove(node);
        addAfterHead(node);
    }

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

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

    private Node removeTail() {
        Node node = tail.prev;
        remove(node);
        return node;
    }
}
type Node struct {
    key, value int
    prev, next *Node
}

type LRUCache struct {
    cap  int
    m    map[int]*Node
    head *Node
    tail *Node
}

func Constructor(capacity int) LRUCache {
    head, tail := &Node{}, &Node{}
    head.next = tail
    tail.prev = head
    return LRUCache{cap: capacity, m: make(map[int]*Node), head: head, tail: tail}
}

func (c *LRUCache) Get(key int) int {
    node, ok := c.m[key]
    if !ok {
        return -1
    }
    c.moveToHead(node)
    return node.value
}

func (c *LRUCache) Put(key int, value int) {
    if node, ok := c.m[key]; ok {
        node.value = value
        c.moveToHead(node)
        return
    }

    node := &Node{key: key, value: value}
    c.m[key] = node
    c.addAfterHead(node)

    if len(c.m) > c.cap {
        lru := c.removeTail()
        delete(c.m, lru.key)
    }
}

func (c *LRUCache) moveToHead(node *Node) {
    c.remove(node)
    c.addAfterHead(node)
}

func (c *LRUCache) addAfterHead(node *Node) {
    node.prev = c.head
    node.next = c.head.next
    c.head.next.prev = node
    c.head.next = node
}

func (c *LRUCache) remove(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (c *LRUCache) removeTail() *Node {
    node := c.tail.prev
    c.remove(node)
    return node
}
class LRUCache {
    struct Node {
        int key, value;
        Node *prev, *next;
        Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
    };

    int capacity;
    unordered_map<int, Node*> mp;
    Node *head, *tail;

    void remove(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void addAfterHead(Node* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    void moveToHead(Node* node) {
        remove(node);
        addAfterHead(node);
    }

    Node* removeTail() {
        Node* node = tail->prev;
        remove(node);
        return node;
    }

public:
    LRUCache(int cap) : capacity(cap) {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        if (!mp.count(key)) return -1;
        Node* node = mp[key];
        moveToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (mp.count(key)) {
            Node* node = mp[key];
            node->value = value;
            moveToHead(node);
            return;
        }

        Node* node = new Node(key, value);
        mp[key] = node;
        addAfterHead(node);

        if ((int)mp.size() > capacity) {
            Node* lru = removeTail();
            mp.erase(lru->key);
            delete lru;
        }
    }
};
class Node:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.map = {}
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head

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

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

    def _move_to_head(self, node: Node) -> None:
        self._remove(node)
        self._add_after_head(node)

    def _remove_tail(self) -> Node:
        node = self.tail.prev
        self._remove(node)
        return node

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

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

        node = Node(key, value)
        self.map[key] = node
        self._add_after_head(node)

        if len(self.map) > self.capacity:
            lru = self._remove_tail()
            del self.map[lru.key]
class Node {
  constructor(key = 0, value = 0) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

var LRUCache = function(capacity) {
  this.capacity = capacity;
  this.map = new Map();
  this.head = new Node();
  this.tail = new Node();
  this.head.next = this.tail;
  this.tail.prev = this.head;
};

LRUCache.prototype._remove = function(node) {
  node.prev.next = node.next;
  node.next.prev = node.prev;
};

LRUCache.prototype._addAfterHead = function(node) {
  node.prev = this.head;
  node.next = this.head.next;
  this.head.next.prev = node;
  this.head.next = node;
};

LRUCache.prototype._moveToHead = function(node) {
  this._remove(node);
  this._addAfterHead(node);
};

LRUCache.prototype._removeTail = function() {
  const node = this.tail.prev;
  this._remove(node);
  return node;
};

LRUCache.prototype.get = function(key) {
  if (!this.map.has(key)) return -1;
  const node = this.map.get(key);
  this._moveToHead(node);
  return node.value;
};

LRUCache.prototype.put = function(key, value) {
  if (this.map.has(key)) {
    const node = this.map.get(key);
    node.value = value;
    this._moveToHead(node);
    return;
  }

  const node = new Node(key, value);
  this.map.set(key, node);
  this._addAfterHead(node);

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

Comments