# 0460. LFU缓存
## 题目地址(460. LFU缓存)
<https://leetcode-cn.com/problems/lfu-cache/>
## 题目描述
```
<pre class="calibre18">```
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。它应该支持以下操作:get 和 put。
get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
put(key, value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除最久未使用的键。
「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。
进阶:
你是否可以在 O(1) 时间复杂度内执行两项操作?
示例:
LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 去除 key 2
cache.get(2); // 返回 -1 (未找到key 2)
cache.get(3); // 返回 3
cache.put(4, 4); // 去除 key 1
cache.get(1); // 返回 -1 (未找到 key 1)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
```
```
## 前置知识
- 链表
- HashMap
## 公司
- 阿里
- 腾讯
- 百度
- 字节
## 思路
`本题已被收录到我的新书中,敬请期待~`
[LFU(Least frequently used)](https://www.wikiwand.com/en/Least_frequently_used) 但内存容量满的情况下,有新的数据进来,需要更多空间的时候,就需要删除被访问频率最少的元素。
举个例子,比如说 cache 容量是 3,按顺序依次放入 `1,2,1,2,1,3`, cache 已存满 3 个元素 (1,2,3), 这时如果想放入一个新的元素 4 的时候,就需要腾出一个元素空间。 用 LFU,这里就淘汰 3, 因为 3 的次数只出现依次, 1 和 2 出现的次数都比 3 多。
题中 `get` 和 `put` 都是 `O(1)`的时间复杂度,那么删除和增加都是`O(1)`,可以想到用双链表,和`HashMap`,用一个`HashMap, nodeMap,` 保存当前`key`,和 `node{key, value, frequent}`的映射。 这样`get(key)`的操作就是`O(1)`. 如果要删除一个元素,那么就需要另一个`HashMap,freqMap,`保存元素出现次数`(frequent)`和双链表`(DoublyLinkedlist)` 映射, 这里双链表存的是 frequent 相同的元素。每次`get`或`put`的时候,`frequent+1`,然后把`node`插入到双链表的`head node, head.next=node`每次删除`freqent`最小的双链表的`tail node, tail.prev`。
用给的例子举例说明:
```
<pre class="calibre18">```
1. put(1, 1),
- 首先查找 nodeMap 中有没有 key=1 对应的 value,
没有就新建 node(key, value, freq) -> node1(1, 1, 1), 插入 nodeMap,{[1, node1]}
- 查找 freqMap 中有没有 freq=1 对应的 value,
没有就新建 doublylinkedlist(head, tail), 把 node1 插入 doublylinkedlist head->next = node1.
如下图,
```
```
![](https://img.kancloud.cn/41/d0/41d066177761cf72a18d4e6f8fa6f9b9_1473x1080.jpg)
```
<pre class="calibre18">```
2. put(2, 2),
- 首先查找 nodeMap 中有没有 key=2 对应的 value,
没有就新建 node(key, value, freq) -> node2(2, 2, 1), 插入 nodeMap,{[1, node1], [2, node2]}
- 查找 freqMap 中有没有 freq=1 对应的 value,
没有就新建 doublylinkedlist(head, tail), 把 node2 插入 doublylinkedlist head->next = node2.
如下图,
```
```
![](https://img.kancloud.cn/a9/ed/a9ed40a71ca074b0b911d2a5c8036f8a_1460x1080.jpg)
```
<pre class="calibre18">```
3. get(1),
- 首先查找 nodeMap 中有没有 key=1 对应的 value,nodeMap:{[1, node1], [2, node2]},
找到 node1,把 node1 freq+1 -> node1(1,1,2)
- 更新 freqMap,删除 freq=1,node1
- 更新 freqMap,插入 freq=2,node1
如下图,
```
```
![](https://img.kancloud.cn/5a/19/5a19bf2fbc4f7a1c22753dfc833e1006_1429x1080.jpg)
```
<pre class="calibre18">```
4. put(3, 3),
- 判断 cache 的 capacity,已满,需要淘汰使用次数最少的元素,找到最小的 freq=1,删除双链表 tail node.prev
如果 tailnode.prev != null, 删除。然后从 nodeMap 中删除对应的 key。
- 首先查找 nodeMap 中有没有 key=3 对应的 value,
没有就新建 node(key, value, freq) -> node3(3, 3, 1), 插入 nodeMap,{[1, node1], [3, node3]}
- 查找 freqMap 中有没有 freq=1 对应的 value,
没有就新建 doublylinkedlist(head, tail), 把 node3 插入 doublylinkedlist head->next = node3.
如下图,
```
```
![](https://img.kancloud.cn/85/ff/85ff5343825d1f17574ad83575a9182c_1425x968.jpg)
```
<pre class="calibre18">```
5. get(2)
- 查找 nodeMap,如果没有对应的 key 的 value,返回 -1。
6. get(3)
- 首先查找 nodeMap 中有没有 key=3 对应的 value,nodeMap:{[1, node1], [3, node3]},
找到 node3,把 node3 freq+1 -> node3(3,3,2)
- 更新 freqMap,删除 freq=1,node3
- 更新 freqMap,插入 freq=2,node3
如下图,
```
```
![](https://img.kancloud.cn/73/d2/73d2db02e42d904918f607aea84fd8a1_1412x973.jpg)
```
<pre class="calibre18">```
7. put(4, 4),
- 判断 cache 的 capacity,已满,需要淘汰使用次数最少的元素,找到最小的 freq=1,删除双链表 tail node.prev
如果 tailnode.prev != null, 删除。然后从 nodeMap 中删除对应的 key。
- 首先查找 nodeMap 中有没有 key=4 对应的 value,
没有就新建 node(key, value, freq) -> node4(4, 4, 1), 插入 nodeMap,{[4, node4], [3, node3]}
- 查找 freqMap 中有没有 freq=1 对应的 value,
没有就新建 doublylinkedlist(head, tail), 把 node4 插入 doublylinkedlist head->next = node4.
如下图,
```
```
![](https://img.kancloud.cn/78/31/78314bfec746b86d7a5fa62a7f03d473_1576x1071.jpg)
```
<pre class="calibre18">```
8. get(1)
- 查找 nodeMap,如果没有对应的 key 的 value,返回 -1。
9. get(3)
- 首先查找 nodeMap 中有没有 key=3 对应的 value,nodeMap:{[4, node4], [3, node3]},
找到 node3,把 node3 freq+1 -> node3(3,3,3)
- 更新 freqMap,删除 freq=2,node3
- 更新 freqMap,插入 freq=3,node3
如下图,
```
```
![](https://img.kancloud.cn/be/8f/be8f7407309adaff7b663587b464ebf7_1434x1080.jpg)
```
<pre class="calibre18">```
10. get(4)
- 首先查找 nodeMap 中有没有 key=4 对应的 value,nodeMap:{[4, node4], [3, node3]},
找到 node4,把 node4 freq+1 -> node4(4,4,2)
- 更新 freqMap,删除 freq=1,node4
- 更新 freqMap,插入 freq=2,node4
如下图,
```
```
![](https://img.kancloud.cn/ed/be/edbe76dedb9a243314eaedf1b94d7b5a_1474x1056.jpg)
## 关键点分析
用两个`Map`分别保存 `nodeMap {key, node}` 和 `freqMap{frequent, DoublyLinkedList}`。 实现`get` 和 `put`操作都是`O(1)`的时间复杂度。
可以用 Java 自带的一些数据结构,比如 HashLinkedHashSet,这样就不需要自己自建 Node,DoublelyLinkedList。 可以很大程度的缩减代码量。
## 代码(Java code)
```
<pre class="calibre18">```
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LC460LFUCache</span> </span>{
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Node</span> </span>{
<span class="hljs-keyword">int</span> key, val, freq;
Node prev, next;
Node(<span class="hljs-keyword">int</span> key, <span class="hljs-keyword">int</span> val) {
<span class="hljs-keyword">this</span>.key = key;
<span class="hljs-keyword">this</span>.val = val;
freq = <span class="hljs-params">1</span>;
}
}
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DoubleLinkedList</span> </span>{
<span class="hljs-keyword">private</span> Node head;
<span class="hljs-keyword">private</span> Node tail;
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> size;
DoubleLinkedList() {
head = <span class="hljs-keyword">new</span> Node(<span class="hljs-params">0</span>, <span class="hljs-params">0</span>);
tail = <span class="hljs-keyword">new</span> Node(<span class="hljs-params">0</span>, <span class="hljs-params">0</span>);
head.next = tail;
tail.prev = head;
}
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">add</span><span class="hljs-params">(Node node)</span> </span>{
head.next.prev = node;
node.next = head.next;
node.prev = head;
head.next = node;
size++;
}
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">remove</span><span class="hljs-params">(Node node)</span> </span>{
node.prev.next = node.next;
node.next.prev = node.prev;
size--;
}
<span class="hljs-title">// always remove last node if last node exists</span>
<span class="hljs-function">Node <span class="hljs-title">removeLast</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">if</span> (size > <span class="hljs-params">0</span>) {
Node node = tail.prev;
remove(node);
<span class="hljs-keyword">return</span> node;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
}
<span class="hljs-title">// cache capacity</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> capacity;
<span class="hljs-title">// min frequent</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> minFreq;
Map<Integer, Node> nodeMap;
Map<Integer, DoubleLinkedList> freqMap;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">LC460LFUCache</span><span class="hljs-params">(<span class="hljs-keyword">int</span> capacity)</span> </span>{
<span class="hljs-keyword">this</span>.minFreq = <span class="hljs-params">0</span>;
<span class="hljs-keyword">this</span>.capacity = capacity;
nodeMap = <span class="hljs-keyword">new</span> HashMap<>();
freqMap = <span class="hljs-keyword">new</span> HashMap<>();
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">get</span><span class="hljs-params">(<span class="hljs-keyword">int</span> key)</span> </span>{
Node node = nodeMap.get(key);
<span class="hljs-keyword">if</span> (node == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">return</span> -<span class="hljs-params">1</span>;
update(node);
<span class="hljs-keyword">return</span> node.val;
}
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">put</span><span class="hljs-params">(<span class="hljs-keyword">int</span> key, <span class="hljs-keyword">int</span> value)</span> </span>{
<span class="hljs-keyword">if</span> (capacity == <span class="hljs-params">0</span>) <span class="hljs-keyword">return</span>;
Node node;
<span class="hljs-keyword">if</span> (nodeMap.containsKey(key)) {
node = nodeMap.get(key);
node.val = value;
update(node);
} <span class="hljs-keyword">else</span> {
node = <span class="hljs-keyword">new</span> Node(key, value);
nodeMap.put(key, node);
<span class="hljs-keyword">if</span> (nodeMap.size() == capacity) {
DoubleLinkedList lastList = freqMap.get(minFreq);
nodeMap.remove(lastList.removeLast().key);
}
minFreq = <span class="hljs-params">1</span>;
DoubleLinkedList newList = freqMap.getOrDefault(node.freq, <span class="hljs-keyword">new</span> DoubleLinkedList());
newList.add(node);
freqMap.put(node.freq, newList);
}
}
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">update</span><span class="hljs-params">(Node node)</span> </span>{
DoubleLinkedList oldList = freqMap.get(node.freq);
oldList.remove(node);
<span class="hljs-keyword">if</span> (node.freq == minFreq && oldList.size == <span class="hljs-params">0</span>) minFreq++;
node.freq++;
DoubleLinkedList newList = freqMap.getOrDefault(node.freq, <span class="hljs-keyword">new</span> DoubleLinkedList());
newList.add(node);
freqMap.put(node.freq, newList);
}
}
```
```
## 参考(References)
1. [LFU(Least frequently used) Cache](https://www.wikiwand.com/en/Least_frequently_used)
2. [Leetcode discussion mylzsd](https://leetcode.com/problems/lfu-cache/discuss/94547/Java-O(1)-Solution-Using-Two-HashMap-and-One-DoubleLinkedList)
3. [Leetcode discussion aaaeeeo](https://leetcode.com/problems/lfu-cache/discuss/94547/Java-O(1)-Solution-Using-Two-HashMap-and-One-DoubleLinkedList)
- Introduction
- 第一章 - 算法专题
- 数据结构
- 基础算法
- 二叉树的遍历
- 动态规划
- 哈夫曼编码和游程编码
- 布隆过滤器
- 字符串问题
- 前缀树专题
- 《贪婪策略》专题
- 《深度优先遍历》专题
- 滑动窗口(思路 + 模板)
- 位运算
- 设计题
- 小岛问题
- 最大公约数
- 并查集
- 前缀和
- 平衡二叉树专题
- 第二章 - 91 天学算法
- 第一期讲义-二分法
- 第一期讲义-双指针
- 第二期
- 第三章 - 精选题解
- 《日程安排》专题
- 《构造二叉树》专题
- 字典序列删除
- 百度的算法面试题 * 祖玛游戏
- 西法的刷题秘籍】一次搞定前缀和
- 字节跳动的算法面试题是什么难度?
- 字节跳动的算法面试题是什么难度?(第二弹)
- 《我是你的妈妈呀》 * 第一期
- 一文带你看懂二叉树的序列化
- 穿上衣服我就不认识你了?来聊聊最长上升子序列
- 你的衣服我扒了 * 《最长公共子序列》
- 一文看懂《最大子序列和问题》
- 第四章 - 高频考题(简单)
- 面试题 17.12. BiNode
- 0001. 两数之和
- 0020. 有效的括号
- 0021. 合并两个有序链表
- 0026. 删除排序数组中的重复项
- 0053. 最大子序和
- 0088. 合并两个有序数组
- 0101. 对称二叉树
- 0104. 二叉树的最大深度
- 0108. 将有序数组转换为二叉搜索树
- 0121. 买卖股票的最佳时机
- 0122. 买卖股票的最佳时机 II
- 0125. 验证回文串
- 0136. 只出现一次的数字
- 0155. 最小栈
- 0167. 两数之和 II * 输入有序数组
- 0169. 多数元素
- 0172. 阶乘后的零
- 0190. 颠倒二进制位
- 0191. 位1的个数
- 0198. 打家劫舍
- 0203. 移除链表元素
- 0206. 反转链表
- 0219. 存在重复元素 II
- 0226. 翻转二叉树
- 0232. 用栈实现队列
- 0263. 丑数
- 0283. 移动零
- 0342. 4的幂
- 0349. 两个数组的交集
- 0371. 两整数之和
- 0437. 路径总和 III
- 0455. 分发饼干
- 0575. 分糖果
- 0874. 模拟行走机器人
- 1260. 二维网格迁移
- 1332. 删除回文子序列
- 第五章 - 高频考题(中等)
- 0002. 两数相加
- 0003. 无重复字符的最长子串
- 0005. 最长回文子串
- 0011. 盛最多水的容器
- 0015. 三数之和
- 0017. 电话号码的字母组合
- 0019. 删除链表的倒数第N个节点
- 0022. 括号生成
- 0024. 两两交换链表中的节点
- 0029. 两数相除
- 0031. 下一个排列
- 0033. 搜索旋转排序数组
- 0039. 组合总和
- 0040. 组合总和 II
- 0046. 全排列
- 0047. 全排列 II
- 0048. 旋转图像
- 0049. 字母异位词分组
- 0050. Pow(x, n)
- 0055. 跳跃游戏
- 0056. 合并区间
- 0060. 第k个排列
- 0062. 不同路径
- 0073. 矩阵置零
- 0075. 颜色分类
- 0078. 子集
- 0079. 单词搜索
- 0080. 删除排序数组中的重复项 II
- 0086. 分隔链表
- 0090. 子集 II
- 0091. 解码方法
- 0092. 反转链表 II
- 0094. 二叉树的中序遍历
- 0095. 不同的二叉搜索树 II
- 0096. 不同的二叉搜索树
- 0098. 验证二叉搜索树
- 0102. 二叉树的层序遍历
- 0103. 二叉树的锯齿形层次遍历
- 105. 从前序与中序遍历序列构造二叉树
- 0113. 路径总和 II
- 0129. 求根到叶子节点数字之和
- 0130. 被围绕的区域
- 0131. 分割回文串
- 0139. 单词拆分
- 0144. 二叉树的前序遍历
- 0150. 逆波兰表达式求值
- 0152. 乘积最大子数组
- 0199. 二叉树的右视图
- 0200. 岛屿数量
- 0201. 数字范围按位与
- 0208. 实现 Trie (前缀树)
- 0209. 长度最小的子数组
- 0211. 添加与搜索单词 * 数据结构设计
- 0215. 数组中的第K个最大元素
- 0221. 最大正方形
- 0229. 求众数 II
- 0230. 二叉搜索树中第K小的元素
- 0236. 二叉树的最近公共祖先
- 0238. 除自身以外数组的乘积
- 0240. 搜索二维矩阵 II
- 0279. 完全平方数
- 0309. 最佳买卖股票时机含冷冻期
- 0322. 零钱兑换
- 0328. 奇偶链表
- 0334. 递增的三元子序列
- 0337. 打家劫舍 III
- 0343. 整数拆分
- 0365. 水壶问题
- 0378. 有序矩阵中第K小的元素
- 0380. 常数时间插入、删除和获取随机元素
- 0416. 分割等和子集
- 0445. 两数相加 II
- 0454. 四数相加 II
- 0494. 目标和
- 0516. 最长回文子序列
- 0518. 零钱兑换 II
- 0547. 朋友圈
- 0560. 和为K的子数组
- 0609. 在系统中查找重复文件
- 0611. 有效三角形的个数
- 0718. 最长重复子数组
- 0754. 到达终点数字
- 0785. 判断二分图
- 0820. 单词的压缩编码
- 0875. 爱吃香蕉的珂珂
- 0877. 石子游戏
- 0886. 可能的二分法
- 0900. RLE 迭代器
- 0912. 排序数组
- 0935. 骑士拨号器
- 1011. 在 D 天内送达包裹的能力
- 1014. 最佳观光组合
- 1015. 可被 K 整除的最小整数
- 1019. 链表中的下一个更大节点
- 1020. 飞地的数量
- 1023. 驼峰式匹配
- 1031. 两个非重叠子数组的最大和
- 1104. 二叉树寻路
- 1131.绝对值表达式的最大值
- 1186. 删除一次得到子数组最大和
- 1218. 最长定差子序列
- 1227. 飞机座位分配概率
- 1261. 在受污染的二叉树中查找元素
- 1262. 可被三整除的最大和
- 1297. 子串的最大出现次数
- 1310. 子数组异或查询
- 1334. 阈值距离内邻居最少的城市
- 1371.每个元音包含偶数次的最长子字符串
- 第六章 - 高频考题(困难)
- 0004. 寻找两个正序数组的中位数
- 0023. 合并K个升序链表
- 0025. K 个一组翻转链表
- 0030. 串联所有单词的子串
- 0032. 最长有效括号
- 0042. 接雨水
- 0052. N皇后 II
- 0084. 柱状图中最大的矩形
- 0085. 最大矩形
- 0124. 二叉树中的最大路径和
- 0128. 最长连续序列
- 0145. 二叉树的后序遍历
- 0212. 单词搜索 II
- 0239. 滑动窗口最大值
- 0295. 数据流的中位数
- 0301. 删除无效的括号
- 0312. 戳气球
- 0335. 路径交叉
- 0460. LFU缓存
- 0472. 连接词
- 0488. 祖玛游戏
- 0493. 翻转对
- 0887. 鸡蛋掉落
- 0895. 最大频率栈
- 1032. 字符流
- 1168. 水资源分配优化
- 1449. 数位成本和为目标值的最大数字
- 后序