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);
*/
