ASE SWE · Phone Coding

Apple LRU Cache:哈希表与双向链表的 O(1) 操作

按容量计条目的训练契约实现 get/put,覆盖更新、命中、淘汰与指针不变量。

题目重点:按容量计条目的训练契约实现 get/put,覆盖更新、命中、淘汰与指针不变量。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版容量按条目数、必须为正;`get(key)` 未命中返回 None,命中刷新最近使用时间;`put` 新增或覆盖,覆盖也刷新;键可哈希。
  • LRU 标准语义只描述近期使用淘汰;若原题按字节限额、TTL、并发、持久化或 `-1` 未命中,就需改契约。

解题路线

  1. 哈希表 key→双向链表节点,链表 head 后是最近使用、tail 前是最久未使用;两个哨兵避免头尾特判。
  2. get 命中时 O(1) 把节点从原位置摘下再插到 head 后;未命中不改变顺序。put 命中更新值并同样刷新;新增后若超容量就删除 tail 前节点及哈希表项。
  3. 不变量:每个表项恰好对应链表中的一个真实节点,正反链接一致,链表从新到旧。任何操作都必须同时维护表和链。
  4. 用 OrderedDict 的独立参考模型对照 27,993 条 get/put 操作序列,逐步检查返回值、顺序和键集合。

手动走一遍

容量 2:put(A,1)、put(B,2) 后 B 最新;get(A) 后 A 最新;put(C,3) 淘汰 B。若再次 put(A,9),A 仍最新且总条目数不增加。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

class Node:
    def __init__(self, key=None, value=None):
        self.key, self.value = key, value
        self.prev = self.next = None


class LRUCache:
    """Capacity in entries; get miss returns None; head is most recent."""
    def __init__(self, capacity: int):
        if capacity < 1:
            raise ValueError("capacity must be positive")
        self.capacity = capacity
        self.nodes = {}
        self.head, self.tail = Node(), Node()
        self.head.next, self.tail.prev = self.tail, self.head

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

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

    def get(self, key):
        node = self.nodes.get(key)
        if node is None:
            return None
        self._unlink(node)
        self._front(node)
        return node.value

    def put(self, key, value) -> None:
        node = self.nodes.get(key)
        if node is not None:
            node.value = value
            self._unlink(node)
            self._front(node)
            return
        node = Node(key, value)
        self.nodes[key] = node
        self._front(node)
        if len(self.nodes) > self.capacity:
            victim = self.tail.prev
            self._unlink(victim)
            del self.nodes[victim.key]

复杂度

哈希表平均 O(1) 查找,双向链表 O(1) 摘插,因此 get/put 平均 O(1),空间 O(capacity);极端哈希冲突不承诺严格最坏 O(1)。

易错点与追问

容易失分的边界

  • 只用数组或普通链表,命中后找节点 O(n)。
  • 更新已有 key 时错误增加容量或不刷新最近使用顺序。
  • 淘汰链表节点却忘记删除哈希表项。
  • 把 head/tail 哨兵当真实条目。
  • 默认线程安全;多线程 get 也会写链表,需锁或分片设计。

继续深挖

  • 若容量按字节计,更新值大小时怎样连续淘汰?
  • TTL 与 LRU 混用时过期条目怎样清理?
  • 多线程读写如何避免指针损坏?
  • LFU 与 LRU 的缓存命中行为在哪些流量下不同?
✓已复制微信号!