一个缓存结构需要实现如下功能:
void set(int key,int value):加入或者修改 key 对应的 value
int get(int key):查询 key 对应的 value 值
但是缓存最多放 K 条记录,如果新的 K + 1 条记录需要加入,就需要根据策略删掉一条记录,然后才能把新记录加入。
这个策略为:在缓存结构的 K 条记录中,哪一个 key 从进入缓存结构的时刻开始,被调用 set 或者 get 次数最少,就删掉这个key 的记录;如果调用次数最少的 key 有多个,上次调用发送最早的 key 被删除。
这个就是 LFU 缓存替换算法。实现这个结构,K 作为参数。
解法一:哈希表 + 有序表
缓存中的数据是 k,v 对,所以用 map 存储数据。由于缓存数据需要根据:使用次数和使用时间进行淘汰,如果遍历数据查找需要淘汰的数据,耗时比较高。因此需要维护一个有序结构,方便查找要淘汰的数据。
- 有序数组:
- 查找要淘汰数据的耗时为:O(1),删除后需要移动数据,耗时为:O(K)
- 每次操作都需要更新 count 和 time 并维护数组的有序,此时也需要查找并移动数据,耗时为为:O(K)
- 有序链表:
- 查找要淘汰数据的耗时为:O(1),(比如头结点或者尾结点)
- 更新操作时,查找对应节点的耗时为:O(K)
- 有序表:
- 查找并移除要淘汰的数据的耗时为:O(log K)
- 更新操作的耗时时为:O(log K)
- 小顶堆:
- 查找并移除要淘汰的数据的耗时为:O(1),移除堆顶后需要堆化的耗时为:O(log K)。
- 更新数据后,也需要堆化,耗时为:O(log K)。
时间复杂度:O(log K)
空间复杂度:O(K)
import java.util.*;
public class LFU1 {
public static class Node implements Comparable<Node> {
public int key;
public int value;
// 这个节点发生get或者set的次数总和
public int count;
// 最后一次操作的时间
public int time;
public Node(int key, int value, int count, int time) {
this.key = key;
this.value = value;
this.count = count;
this.time = time;
}
// 缓存淘汰优先级
// 最少使用(count 越小越容易被淘汰)
// count 相同,时间越早越容易被淘汰(time 越小越容易被淘汰)
@Override
public int compareTo(Node o) {
return count == o.count ? Integer.compare(time, o.time) : Integer.compare(count, o.count);
}
@Override
public String toString() {
return "Node{" + "key=" + key + ", value=" + value + ", count=" + count + ", time=" + time + '}';
}
}
public static class LFUCache {
// 缓存过期优先级
TreeSet<Node> set = new TreeSet<>();
Map<Integer, Node> map = new HashMap<>();
int capacity;
int time = 0; // 用来计算缓存时间
public LFUCache(int capacity) {
this.capacity = Math.max(capacity, 0);
}
public Integer get(int key) {
if (!map.containsKey(key)) {
return null;
}
Node node = map.get(key);
set(key, node.value);
return node.value;
}
public void set(int key, int value) {
this.time += 1;
// 更新
if (map.containsKey(key)) {
Node node = map.get(key);
// 删除再插入(node 的count 和 time 变化了,TreeSet 认为是不同的数据)
set.remove(node);
node.time = this.time;
node.count++;
node.value = value;
set.add(node);
map.put(key, node);
return;
}
// 新增
// 如果内存满了,淘汰一条旧数据
if (this.capacity == this.map.size()) {
remove();
}
Node node = new Node(key, value, 1, this.time);
map.put(key, node);
set.add(node);
}
public void remove() {
if (map.size() == 0) {
return;
}
Node node = set.first();
map.remove(node.key);
set.remove(node);
}
}
}
解法二:哈希表 + 小顶堆
将有序表更换为小顶堆。
删除数据时,heap.pop()
更新数据后,堆化:heap.heapify(node)。更新数据使得 time 和 count 变大,因此只需要从 node 节点开始向下堆化。
时间复杂度:O(log K)
空间复杂度:O(K)
import java.util.*;
public class LFU3 {
public static class Node {
public int key;
public int value;
// 这个节点发生get或者set的次数总和
public int count;
// 最后一次操作的时间
public int time;
public Node(int key, int value, int count, int time