在问 · 算法题解

460. LFU 缓存

困难设计哈希表 + 按频率分组的有序链表力扣原题 ↗

题目描述

实现一个按“最不经常使用”(LFU)策略淘汰的缓存类 LFUCache。每个键都附带一个使用次数:首次通过 put 放入时记为 1,之后每次对它调用 get 或 put 都加 1。

LFUCache(capacity):创建容量为 capacity 的缓存。get(key):键存在时返回对应的值,否则返回 -1。

put(key, value):键已存在就改写它的值;不存在就插入。如果插入前缓存已经装满,先移除使用次数最少的键;若有多个键次数并列最少,移除其中最久没被访问的那个。put 无返回值。

get 和 put 的平均时间复杂度都要求是 O(1)。

示例 1
输入:操作 = ["LFUCache","put","put","get","put","get","get","put","get","get","get"]
参数 = [[2],[1,1],[2,2],[1],[3,3],[2],[3],[4,4],[1],[3],[4]]
输出:[null,null,null,1,null,-1,3,null,-1,3,4]

解释:容量为 2。放入 3 时淘汰次数最少的键 2;放入 4 时键 1 和 3 次数都是 2,淘汰较久未用的键 1,所以之后 get(1) 和 get(2) 都返回 -1。

数据范围

  • 1 <= capacity <= 10^4
  • 0 <= key <= 10^5
  • 0 <= value <= 10^9
  • get 与 put 合计调用不超过 2 * 10^5 次。

解题思路:哈希表 + 按频率分组的有序链表

维护三张表和一个变量:val 存值,cnt 存使用次数,groups[f] 是使用次数为 f 的键组成的有序集合(按最近使用时间排列,最久的在前),minf 是当前最小使用次数。一次使用(touch)把键从 groups[f] 移到 groups[f+1] 的末尾;如果 groups[minf] 因此变空,minf 加 1。插入新键时如果满了,淘汰 groups[minf] 的第一个键;新键次数为 1,所以 minf 重置为 1。

为什么正确

每个键恰好在 groups[cnt[key]] 中;各组内部按最近使用时间从旧到新;minf 是非空组中最小的次数。

复杂度分析

get 和 put 均为 O(1);空间 O(capacity)。

易错点

更新已有键的值也要算一次使用;插入新键前要先淘汰再把 minf 置为 1,顺序反了会淘汰错组;touch 时只有当变空的是 groups[minf] 才能让 minf 加 1。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class LFUCache:
    def __init__(self, capacity: int = 2):
        self.cap = capacity
        self.val = {}
        self.cnt = {}
        self.groups = {}
        self.minf = 0

    def _touch(self, key: int) -> None:
        f = self.cnt[key]
        del self.groups[f][key]
        if not self.groups[f]:
            del self.groups[f]
            if self.minf == f:
                self.minf = f + 1
        self.cnt[key] = f + 1
        self.groups.setdefault(f + 1, {})[key] = None

    def get(self, key: int) -> int:
        if key not in self.val:
            return -1
        self._touch(key)
        return self.val[key]

    def put(self, key: int, value: int) -> None:
        if key in self.val:
            self.val[key] = value
            self._touch(key)
            return
        if len(self.val) == self.cap:
            old = next(iter(self.groups[self.minf]))
            del self.groups[self.minf][old]
            if not self.groups[self.minf]:
                del self.groups[self.minf]
            del self.val[old]
            del self.cnt[old]
        self.val[key] = value
        self.cnt[key] = 1
        self.groups.setdefault(1, {})[key] = None
        self.minf = 1

载入编辑器,自己跑一遍 →

其他解法

频率分组链表做到严格 O(1),但指针和最小频率的维护容易出错;小根堆把“频率最低、最久未用”交给堆排序,代码简单但慢一个 log。

解法 2:哈希表 + 小根堆(懒删除) 时间 O(log m),空间 O(m)

用哈希表记录每个键的值、使用频率和最近访问时间戳。每次访问都把(频率, 时间戳, 键)压入小根堆,不删除旧记录。需要淘汰时不断弹出堆顶,只有与键当前频率和时间戳都一致的记录才有效,删除这个键。m 为操作次数。

不需要维护复杂的链表和最小频率,思路直接,容易写对。每次操作是对数级,堆里会堆积过期记录,空间与操作次数相关。

class LFUCache:
    def __init__(self, capacity: int = 2):
        self.cap = capacity
        self.val = {}
        self.freq = {}
        self.tick = {}   # 键最近一次被访问的时间戳
        self.clock = 0
        self.heap = []   # (频率, 时间戳, 键),旧记录懒删除

    def _touch(self, key: int, f: int) -> None:
        self.clock += 1
        self.freq[key] = f
        self.tick[key] = self.clock
        heapq.heappush(self.heap, (f, self.clock, key))

    def get(self, key: int) -> int:
        if key not in self.val:
            return -1
        self._touch(key, self.freq[key] + 1)
        return self.val[key]

    def put(self, key: int, value: int) -> None:
        if key in self.val:
            self.val[key] = value
            self._touch(key, self.freq[key] + 1)
            return
        if len(self.val) == self.cap:
            while True:
                f, t, k = heapq.heappop(self.heap)
                # 只有与键当前状态一致的记录才有效
                if k in self.val and self.freq[k] == f and self.tick[k] == t:
                    del self.val[k], self.freq[k], self.tick[k]
                    break
        self.val[key] = value
        self._touch(key, 1)

设计 专题的其他题目

查看 设计 全部题目 →