https://leetcode-cn.com/problems/lfu-cache/description/
缓存的实现可以采取多种策略,不同策略优点的评估就是“命中率”。好的策略可以实现较高的命中率。常用的策略如:LRU(最近最少使用)、LFU(最不频繁使用)。这两种策略都可以在O(1)时间内实现get和put。关于LRU,在 http://www.cnblogs.com/weiyinfu/p/8546080.html 中已经介绍过。本文主要讲讲LFU的实现。
LFU比LRU复杂,为什么这么说呢?当每个元素只访问一次,则各个元素的使用频率都是1,这是遵循的法则是LRU,即越早被访问的元素越先被删除。LRU的实现可以用Java中的LinkedHashSet实现。
这里复习一下三种Set的区别和联系:
- HashSet:哈希集,元素无序,读写O(1)
- TreeSet:元素有序,读写都是O(lgN)
- LinkedHashSet:双向链表+哈希集,元素有序,元素的顺序为插入的顺序,读写复杂度O(1)
方法一:使用LinkedHashSet实现LRU
第一种方法:三个哈希,使用HashSet实现LRU,因为HashSet中的元素使用的是Integer,可以在HashSet上直接实现LRU;如果HashSet中的元素使用的是Node,则无法直接从HashSet中删除元素。
LFU的关键思路:
- 对于新插入的元素,它的使用频率是1。如果缓存满了,必须在插入新元素之前移除掉旧元素而不能在插入新元素之后移除最低频使用的元素,因为那样可能会把刚刚插入的新元素删掉。
- 只需要一个min记录当前使用频次最低的元素,如果新元素来之前队列满了,肯定要删除掉这个min元素,而不是其它使用频次较高的元素。即便这个min元素以后使用频次超过了“倒数第二”,在超过之前一定可以遇到“倒数第二”。
- LFU需要LRU作为桶,盛放那些使用频次相同的元素。
这段程序的技巧性在于只使用Integer而不使用自定义类型。
import java.util.HashMap;
import java.util.LinkedHashSet;
class LFUCache {
public int capacity;//容量大小
public HashMap<Integer, Integer> map = new HashMap<>();//存储put进去的key和value
public HashMap<Integer, Integer> frequent = new HashMap<>();//存储每个key的频率值
//存储每个频率的相应的key的值的集合,这里用HashSet是因为其是由HashMap底层实现的,可以O(1)时间复杂度查找元素
//而且linked是有序的,同一频率值越往后越最近访问
public HashMap<Integer, LinkedHashSet<Integer>> list = new HashMap<>();
int min = -1;//标记当前频率中的最小值
public LFUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if(!map.containsKey(key)){
return -1;
}else{
int value = map.get(key);//获取元素的value值
int count = frequent.get(key);
frequent.put(key, count + 1);
list.get(count).remove(key);//先移除当前key
//更改min的值
if(count == min && list.get(count).size() == 0)
min++;
LinkedHashSet<Integer> set = list.containsKey(count + 1) ? list.get(count + 1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(count + 1, set);
return value;
}
}
public void put(int key, int value) {
if(capacity <= 0){
return;
}
//这一块跟get的逻辑一样
if(map.containsKey(key)){
map.put(key, value);
int count = frequent.get(key);
frequent.put(key, count + 1);
list.get(count).remove(key);//先移除当前key
//更改min的值
if (count == min && list.get(count).size() == 0)
min++;
LinkedHashSet<Integer> set = list.containsKey(count + 1) ? list.get(count + 1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(count + 1, set);
}else{
if(map.size() >= capacity){
Integer removeKey = list.get(min).iterator().next();
list.get(min).remove(removeKey);
map.remove(removeKey);
frequent.remove(removeKey);
}
map.put(key, value);
frequent.put(key, 1);
LinkedHashSet<Integer> set = list.containsKey(1) ? list.get(1) : new LinkedHashSet<Integer>();
set.add(key);
list.put(1, set);
min = 1;
}
}
public static void main(String[] args) {
LFUCache lfuCache = new LFUCache(2);
lfuCache.put(2, 1);
lfuCache.put(3, 2);
System.out.println(lfuCache.get(3));
System.out.println(lfuCache.get(2));
lfuCache.put(4, 3);
System.out.println(lfuCache.get(2));
System.out.println(lfuCache.get(3));
System.out.println(lfuCache.get(4));
}
}
方法二:使用LinkedHashMap实现LRU
方法其实跟方法一是一样的,方法一使用LinkedHashSet+HashMap实现LRU,实际上完全可以改为LinkedHashMap<Integer,Integer>
,这样就能够使用两个组件:frequencyMap
和HashMap<frequency,LRU>
来实现LFU。
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
class LFUCache {
//key出现的频率为value
HashMap<Integer, Integer> frequency = new HashMap<>();
//频率为key的hashMap为value
HashMap<Integer, LinkedHashMap<Integer, Integer>> a = new HashMap<>();
//时刻记住需要更新哪些全局变量
int min = 0;//最小频率
int capacity;//容器的容量
int nowsize = 0;//当前容器中元素个数
public LFUCache(int capacity) {
this.capacity = capacity;
}
public String tos(Map<Integer, Integer> ma) {
StringBuilder builder = new StringBuilder();
for (int i : ma.keySet()) {
builder.append(i + ":" + ma.get(i) + " ");
}
return builder.toString();
}
public void debug() {
System.out.println(tos(frequency));
for (int i : a.keySet()) {
System.out.println(i + " " + tos(a.get(i)));
}
System.out.println("======");
}
public int get(int key) {
Integer f = frequency.get(key);
if (f == null) {
return -1;
}
int value = a.get(f).get(key);
active(key);//激活一下key,使其频率+1
return value;
}
void active(int key) {
int f = frequency.get(key);
frequency.put(key, f + 1);
LinkedHashMap<Integer, Integer> src = a.get(f), des = a.getOrDefault(f + 1, new LinkedHashMap<>());
des.put(key, src.remove(key));
tryRemove(f);
a.put(f + 1, des);
}
void tryRemove(int frequency) {
if (a.get(frequency).size() == 0) {
if (frequency == min) {
min++;
}
a.remove(frequency);
}
}
void removeLFU() {
LinkedHashMap<Integer, Integer> ma = a.get(min);
int removing = ma.keySet().iterator().next();
ma.remove(removing);//移除掉最早插入的那个结点
tryRemove(min);
frequency.remove(removing);
nowsize--;
}
public void put(int key, int value) {
if (capacity == 0) return;
if (frequency.get(key) == null) {
if (capacity == nowsize) removeLFU();
nowsize++;
frequency.put(key, 1);
LinkedHashMap<Integer, Integer> ff = a.getOrDefault(1, new LinkedHashMap<>());
ff.put(key, value);
a.put(1, ff);
min = 1;//新插入结点之后,最低频率必然为1
} else {
active(key);
a.get(frequency.get(key)).put(key, value);
}
}
public static void main(String[] args) {
LFUCache cache = new LFUCache(2);
String[] op = {"put", "put", "get", "put", "get", "get", "put", "get", "get", "get"};
int[][] value = {{1, 1}, {2, 2}, {1}, {3, 3}, {2}, {3}, {4, 4}, {1}, {3}, {4}};
for (int i = 0; i < op.length; i++) {
System.out.println(op[i] + " " + value[i] + " " + cache.min);
cache.debug();
if (op[i].equals("put")) {
cache.put(value[i][0], value[i][1]);
} else {
cache.get(value[i][0]);
}
}
}
}
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
方法三:最佳复杂度
在上面的方法中,重要缺点之一就是空间复杂度略微有点高,因为每一个LRU都是使用HashMap实现的,而每一个频率对应一个LRU,这就导致当使用的频率种数很多时,HashMap很多,造成空间巨大浪费。
LFU跟LRU思路是一样的,把最近使用过的东西从左往右排成一排(右面的频率比较高),当使用一个元素之后,把这个元素频率加1,向右面移动几格。应该移动到什么地方呢?这需要快速定位,所以需要快速找到每个频率的最后一个元素,这可以通过建立一个频率到结点的映射来实现。
import java.util.HashMap;
import java.util.Map;
class LFUCache {
//定义双向链表的结点
class Node {
Node prev, next;
int key, value;
int frequency;
Node(int key, int value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "(" + key + ":" + value + " " + frequency + ")";
}
}
//定义双向链表
class LinkedList {
Node head, tail;
LinkedList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
//移除双向链表中的结点
void remove(Node node) {
Node prev = node.prev;
Node next = node.next;
prev.next = next;
next.prev = prev;
}
//在who之后插入newNode
void insertAfter(Node who, Node newNode) {
Node next = who.next;
who.next = newNode;
newNode.next = next;
next.prev = newNode;
newNode.prev = who;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
for (Node i = head.next; i != tail; i = i.next) {
builder.append(String.format("(%d:%d,%d)->", i.key, i.value, i.frequency));
}
return builder.toString();
}
}
//缓存的容量
int capacity;
//双向链表
LinkedList link = new LinkedList();
//key到Node的映射
Map<Integer, Node> ma = new HashMap<>();
//频率到尾节点的映射
Map<Integer, Node> tail = new HashMap<>();
int nowsize = 0;
public LFUCache(int capacity) {
this.capacity = capacity;
link.head.frequency = 0;
link.tail.frequency = Integer.MAX_VALUE;
tail.put(link.head.frequency, link.head);
tail.put(link.tail.frequency, link.tail);
}
String tos(Map<Integer, Node> ma) {
StringBuilder builder = new StringBuilder();
for (int i : ma.keySet()) {
builder.append(i + ":" + ma.get(i) + " ");
}
return builder.toString();
}
void debug() {
System.out.println(link.toString());
System.out.println(tos(tail));
System.out.println(tos(ma));
System.out.println("========");
}
public int get(int key) {
Node node = ma.get(key);
if (node == null) {
return -1;
}
active(node);//命中,激活之
return node.value;
}
void active(Node node) {
int f = node.frequency;
node.frequency++;
Node prev = node.prev;
Node master = tail.get(f);//当前频率的老大
Node masterNext = master.next;//当前老大的下一个
if (node == master) {
if (prev.frequency == f) {//我是老大,后继有人
tail.put(f, prev);
} else {//我是老大,后继无人
tail.remove(f);
}
if (masterNext.frequency == f + 1) {//下一组频率相邻
link.remove(node);
link.insertAfter(tail.get(f + 1), node);
tail.put(f + 1, node);
} else {//下一组频率不相邻,链表结构不变
tail.put(f + 1, node);
}
} else {//我不是老大
if (masterNext.frequency == f + 1) {//下一组频率相邻
link.remove(node);
link.insertAfter(tail.get(f + 1), node);
tail.put(f + 1, node);
} else {//下一组频率不相邻
link.remove(node);
link.insertAfter(master, node);
tail.put(f + 1, node);
}
}
}
//移除掉最近最少使用的结点
void removeLFU() {
Node node = link.head.next;
Node next = node.next;
link.remove(node);
ma.remove(node.key);
if (node.frequency != next.frequency) {
tail.remove(node.frequency);
}
}
public void put(int key, int value) {
if (capacity == 0) return;
Node node = ma.get(key);
if (node == null) {
if (nowsize >= capacity) {//容量超了,移除LFU
removeLFU();
nowsize--;
}
Node newNode = new Node(key, value);
newNode.frequency = 1;
Node oneMaster = tail.get(1);//使用频率为1的group
if (oneMaster == null) {
link.insertAfter(link.head, newNode);
} else {
link.insertAfter(tail.get(1), newNode);
}
nowsize++;
tail.put(1, newNode);
ma.put(key, newNode);
} else {
active(node);
node.value = value;
}
}
public static void main(String[] args) {
LFUCache cache = new LFUCache(3 /* capacity (缓存容量) */);
String[] ops = {"put", "put", "put", "put", "get"};
int[][] values = {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {4}};
for (int i = 0; i < ops.length; i++) {
System.out.println(ops[i] + " " + values[i][0]);
if (ops[i].equals("put")) {
cache.put(values[i][0], values[i][1]);
} else {
int res = cache.get(values[i][0]);
System.out.println(res);
}
cache.debug();
}
}
}
/**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/