LeetCode 460. LFU 缓存
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache
类:
LFUCache(int capacity)
- 用数据结构的容量capacity
初始化对象int get(int key)
- 如果键存在于缓存中,则获取键的值,否则返回 -1。void put(int key, int value)
- 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get
和 put
函数的次数之和。使用次数会在对应项被移除后置为 0 。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1
(由于 put 操作)。对缓存中的键执行 get
或 put
操作,使用计数器的值将会递增。
示例:
输入:
["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]
解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lFUCache = new LFUCache(2);
lFUCache.put(1, 1); // cache=[1,_], cnt(1)=1
lFUCache.put(2, 2); // cache=[2,1], cnt(2)=1, cnt(1)=1
lFUCache.get(1); // 返回 1
// cache=[1,2], cnt(2)=1, cnt(1)=2
lFUCache.put(3, 3); // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
// cache=[3,1], cnt(3)=1, cnt(1)=2
lFUCache.get(2); // 返回 -1(未找到)
lFUCache.get(3); // 返回 3
// cache=[3,1], cnt(3)=2, cnt(1)=2
lFUCache.put(4, 4); // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
// cache=[4,3], cnt(4)=1, cnt(3)=2
lFUCache.get(1); // 返回 -1(未找到)
lFUCache.get(3); // 返回 3
// cache=[3,4], cnt(4)=1, cnt(3)=3
lFUCache.get(4); // 返回 4
// cache=[3,4], cnt(4)=2, cnt(3)=3
提示:
0 <= capacity, key, value <= 104
- 最多调用
105
次get
和put
方法
解题思路
对比 Medium | LeetCode 146. LRU 缓存机制 | HashMap+双向链表 | LinkedHasp 有一些共同点。但是还有有很大的不同。
首先借鉴前题的思路, 为了保证get的复杂度是O(1), 这个时候需要使用一个HashMap去保存key和value的值。
这道题的关键是要动态的更新访问次数, 然后在缓存满需要替换时, 换掉访问次数最少的。所以对此问题展开了如下的分析。
思路一
为了找到访问次数最少的, 第一反应是使用优先队列的方式。因为优先队列可以在O(1)时间弹出访问最少的节点。
但是存在以下两个问题:
1、优先队列可以找到访问最少的节点, 但是弹出过后, 还有做一个堆调整的过程。这个复杂度是O(logn)的
2、Put一个新key的时候, 访问次数是1, 添加到优先队列时, 需要从堆底逐渐将这个节点调整到堆顶, 这个复杂度是O(logn)
3、当访问某个非堆顶节点, 需要修改这个节点的访问次数, 然后重新调整它在优先队列的位置时, 是没有操作办法的。优先队列既不会自动调整, 我们也没有办法将这个点删除掉再插入调整。
上面第3个问题是致命的, 不仅仅是复杂度不达标, 更是最基本的实现都无法满足。
所以此题不能使用优先队列, 应当使用的是TreeSet方法。这是一个有序的Set, 底层采用红黑树的存储结构。删除, 添加的操作均是O(logn)。所以上述的第三个问题可以使用TreeSet解决。1, 2只是复杂度没有达到要求。
同时此题要求 在相同次数下, 依据LRU算法进行淘汰。所以这个时候可以在节点中添加一个time字段, 并且在缓存当中维持一个全局的time。每个访问某个节点时, 将当前访问的节点的time依据全局的time进行调整。
public class LFUCache {
static class LFUNode implements Comparable {
int visitCount;
int key;
int value;
int time; // 最近的访问时间
public LFUNode(int key, int value, int time) {
this.key = key;
this.value = value;
this.time = time;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
} else {
return this.time - node.time;
}
}
}
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
private final HashMap<Integer, LFUNode> map = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int time; // 全局的时间字段, 每次get put 此字段自增
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
this.time = 0;
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
int curTime = refreshCurrentTime();
LFUNode node = map.get(key);
if (node != null) {
// Cache命中
// 先更新值
queue.remove(node);
node.value = value;
node.time = curTime;
// 更新访问次数, 由于红黑树当中修改节点值并不会修改顺序
// 所以需要将此节点从红黑树中删除再次插入
node.visitCount++;
queue.add(node);
return;
}
// Cache没有命中
if (curCapacity == MAX_CAPACITY) {
LFUNode popNode = queue.pollFirst();
map.remove(popNode.key);
curCapacity--;
}
LFUNode newNode = new LFUNode(key, value, curTime);
newNode.visitCount = 1;
queue.add(newNode);
this.curCapacity++;
map.put(key, newNode);
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
int curTime = refreshCurrentTime();
LFUNode node = map.get(key);
if (node != null) {
// Cache命中
// 更新访问次数, 由于红黑树当中修改节点值并不会修改顺序
// 所以需要将此节点从红黑树中删除再次插入
queue.remove(node);
// 必须先移除才能修改访问次数, 如果先修改访问次数再移除会移除失败
node.visitCount++;
node.time = curTime;
// 这里也是, 在add前必须将visitCount和time修改完, 然后add。add之后修改是不会改变顺序的
queue.add(node);
return node.value;
}
return -1;
}
public int refreshCurrentTime() {
return ++this.time;
}
}
思路二
为了找到次数最少的, 还有一种办法是将访问次数通过HashMap保存起来。以<Freq, List<Node>>的形式保存。因为有多个节点具有相同的访问次数。并且要求相同次数下, 需要使用LRU进行替换。 所以借鉴前一题当中的LRU的算法, 这里需要使用一个双向链表来保存相同次数的节点
在进行get和put时, 访问次数会发生变化, 需要将原节点将其所在的双向链表当中删除, 并且更改访问次数, 添加到新的双向链表的头部。
在访问空间满是需要淘汰时, 可以记录一个全局的minFreq来表示<Freq, List<Node>>的map里双向链表非空的最小的Freq。这个minFreq的维护策略是, 当put新的key(添加)时, minFreq = 1。当put更新已有的key, 或者get命中时, 将此节点删除的时候, 判断删除后双向链表是否为空。如果为空了并且minFreq和当前的访问次数相同, 则将minFreq自增。然后在缓存淘汰时, 由于要添加新节点, 所以minFreq必须变成1, 所以在淘汰缓存时, 不需要对minFreq做调整。
public class LFUCache {
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
// keyMap 用来保存key和value的映射
private final HashMap<Integer, LFUNode> keyMap = new HashMap<>();
// countMap 用来保存访问次数 的 节点, 主要用来做淘汰策略
private final HashMap<Integer, Deque<LFUNode>> countMap = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int minFreq;
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
}
// incVisitCount 访问次数自增, 并且调整此节点在双向链表的位置
public void incVisitCount(LFUNode node) {
int visitCount = node.visitCount;
Deque<LFUNode> nodes = countMap.get(visitCount);
nodes.remove(node);
// 更新访问次数时, 需要更新最小的访问次数
if (nodes.size() == 0 && minFreq == visitCount) {
minFreq++;
}
node.visitCount = ++visitCount;
nodes = countMap.get(visitCount);
if (nodes == null) {
nodes = new LinkedList<>();
countMap.put(visitCount, nodes);
}
nodes.addFirst(node);
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
LFUNode node = keyMap.get(key);
if (node != null) {
// Cache命中
node.value = value;
incVisitCount(node);
return;
}
// Cache未命中
if (curCapacity == MAX_CAPACITY) {
// Cache已满, 淘汰访问次数最少的, 并且最久未访问的
LFUNode popNode = countMap.get(minFreq).removeLast();
keyMap.remove(popNode.key);
curCapacity--;
}
// 添加新的节点
LFUNode newNode = new LFUNode(key, value);
minFreq = 1;
keyMap.put(key, newNode);
Deque<LFUNode> sameCntNodes = countMap.get(1);
if (sameCntNodes == null) {
sameCntNodes = new LinkedList<>();
countMap.put(1, sameCntNodes);
}
sameCntNodes.addFirst(newNode);
curCapacity++;
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
LFUNode node = keyMap.get(key);
if (node != null) {
incVisitCount(node);
return node.value;
}
return -1;
}
}
class LFUNode implements Comparable {
int visitCount;
int key;
int value;
public LFUNode(int key, int value) {
this.key = key;
this.value = value;
this.visitCount = 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
}
return 0;
}
}
上述答案没有问题, 但是LeetCode提交代码时, 显示在时间上只打败了5%的对手, 检查代码才发现LinkedList的remove(Object o)方法是通过遍历的方式找到key相等的节点然后删除的。所以时间复杂度很高。所以要想取得一个高效的结果, 需要自己写一个双向链表。
public class LFUCache {
static class LFUNode implements Comparable {
int visitCount;
int key;
int value;
LFUNode prev;
LFUNode next;
public LFUNode() {
}
public LFUNode(int key, int value) {
this.key = key;
this.value = value;
this.visitCount = 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LFUNode lfuNode = (LFUNode) o;
return key == lfuNode.key;
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public int compareTo(Object o) {
if (this == o) return 0;
if (o == null || getClass() != o.getClass()) return 0;
LFUNode node = (LFUNode) o;
if (this.visitCount != node.visitCount) {
return this.visitCount - node.visitCount;
}
return 0;
}
}
static class DoubleLinkedList {
private LFUNode head, tail;
public DoubleLinkedList() {
head = new LFUNode();
tail = new LFUNode();
head.next = tail;
}
public void unlink(LFUNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
public void addToHead(LFUNode node) {
node.prev = head;
node.next = head.next;
head.next = node;
node.next.prev = node;
}
public boolean isEmpty() {
return tail == head.next;
}
public LFUNode removeLast() {
LFUNode toDeletedNode = tail.prev;
unlink(toDeletedNode);
return toDeletedNode;
}
}
private final TreeSet<LFUNode> queue = new TreeSet<>(LFUNode::compareTo);
// keyMap 用来保存key和value的映射
private final HashMap<Integer, LFUNode> keyMap = new HashMap<>();
// countMap 用来保存访问次数 的 节点, 主要用来做淘汰策略
private final HashMap<Integer, DoubleLinkedList> countMap = new HashMap<>();
private int curCapacity = 0;
private final int MAX_CAPACITY;
private int minFreq;
public LFUCache(int maxCapacity) {
this.MAX_CAPACITY = maxCapacity;
}
public void incVisitCount(LFUNode node) {
int visitCount = node.visitCount;
DoubleLinkedList nodes = countMap.get(visitCount);
nodes.unlink(node);
// 更新访问次数时, 需要更新最小的访问次数
if (nodes.isEmpty() && minFreq == visitCount) {
minFreq++;
}
node.visitCount = ++visitCount;
nodes = countMap.get(visitCount);
if (nodes == null) {
nodes = new DoubleLinkedList();
countMap.put(visitCount, nodes);
}
nodes.addToHead(node);
}
public void put(int key, int value) {
if (MAX_CAPACITY == 0) {
return;
}
LFUNode node = keyMap.get(key);
if (node != null) {
// Cache命中
node.value = value;
incVisitCount(node);
return;
}
// Cache未命中
if (curCapacity == MAX_CAPACITY) {
// Cache已满, 淘汰访问次数最少的, 并且最久未访问的
LFUNode popNode = countMap.get(minFreq).removeLast();
keyMap.remove(popNode.key);
curCapacity--;
}
LFUNode newNode = new LFUNode(key, value);
minFreq = 1;
keyMap.put(key, newNode);
DoubleLinkedList sameCntNodes = countMap.get(1);
if (sameCntNodes == null) {
sameCntNodes = new DoubleLinkedList();
countMap.put(1, sameCntNodes);
}
sameCntNodes.addToHead(newNode);
curCapacity++;
}
public int get(int key) {
if (MAX_CAPACITY == 0) {
return -1;
}
LFUNode node = keyMap.get(key);
if (node != null) {
incVisitCount(node);
return node.value;
}
return -1;
}
}