问题描述
设计并实现最不经常使用(LFU)缓存的数据结构。它应该支持以下操作:get 和 put。
- get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。
- put(key, value) -如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,使最不经常使用的项目无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,最近最少使用的键将被去除。
进阶:
- 你是否可以在 O(1) 时间复杂度内执行两项操作?
思路
这题可以在结点中设置两个字段,一个是lastRead,自增型的,frequency,代表了访问频度。通过这两个字段,一次遍历我们就能拿到频度最小且lastRead最小的值。O(n)的时间就能搞定。(方法一)
参考LRU的做法,我们可以这样做:
按照频度建立一个双向链表(我们称它为横着的双向链表)。离头结点最近的是频率最小的结点。离tail结点最近的是频率最大的结点。
对于这个双向链表中的每一个结点中,我们设置另外一个双向链表,即纵向链表。这个链表中存储的是我们真正的Value值。离头结点最近的是这个频率里最之前访问的结点,离tail结点最近的是这个频率里最后访问的结点。即,同样频率的结点都被存储在同一个纵向双向链表中。
通过横向的双向链表,我们可以在O(1)的时间内找到频率最小的那个双向链表。
通过纵向链表,我们可以在O(1)的时间内找到最早访问的结点。而双向链表保证了我们在更新链表的时间花费是O(1)。(方法二)
方法一
class Value{
int val;
int lastRead;
int frequecy;
Value(int val){
this.val = val;
}
}
class LFUCache {
Value[] cache;
Map<Integer,Value> map;
int length;
int count;
public LFUCache(int capacity) {
length = capacity;
map = new HashMap<>(capacity);
}
public int get(int key) {
if(map.containsKey(key)){
map.get(key).lastRead = count++;
map.get(key).frequecy++;
return map.get(key).val;
}
return -1;
}
public void put(int key, int value) {
if(get(key) != -1) {
map.get(key).val = value;
return;
}
if(map.size() == length){
// 达到最大容量
int minFrequency = Integer.MAX_VALUE;
int targetKey = -1;
for(Integer k: map.keySet()){
if(map.get(k).frequecy < minFrequency){
minFrequency = map.get(k).frequecy;
targetKey = k;
}else if(map.get(k).frequecy == minFrequency){
if(map.get(k).lastRead < map.get(targetKey).lastRead){
targetKey = k;
}
}
}
map.remove(targetKey);
}
if(map.size() < length){
map.put(key,new Value(value));
map.get(key).lastRead = count++;
}
}
}
方法二
import java.util.*;
public class Main{
public static void main(String[] args){
LFUCache lfuCache = new LFUCache(2);
lfuCache.put(1,1);
lfuCache.put(2,2);
lfuCache.get(1);
}
}
class Value{
int key;
int val;
int frequency;
Value(int key,int val){
this.key = key;
this.val = val;
}
// 供纵向双向链表用
Value pre;
Value next;
}
class DLHead{
int frequency;
// 纵向链表的头和尾 建立纵向链表
Value head;
Value tail;
int length;
DLHead(int frequency){
this.frequency = frequency;
head = new Value(-1,-1);
tail = new Value(-1,-1);
head.next = tail;
tail.pre = head;
}
// 供横向链表使用
DLHead pre;
DLHead next;
}
class LFUCache {
Map<Integer, Value> cache;
Map<Integer, DLHead> freq;
int length;
// 建立横向链表
DLHead head;
DLHead tail;
public LFUCache(int capacity) {
cache = new HashMap<>(capacity);
freq = new HashMap<>();
length = capacity;
head = new DLHead(-1);
tail = new DLHead(-1);
head.next = tail;
tail.pre = head;
}
public int get(int key) {
if(cache.containsKey(key)){
// 增加频次
updateFreq(key);
return cache.get(key).val;
}
return -1;
}
public void put(int key, int value) {
if(length == 0) return;
if(cache.containsKey(key)){
// key 已存在,则更新val,更新频次
cache.get(key).val = value;
updateFreq(key);
}else{
// key 不在,判定是否满了
if(cache.size() == length){
// 删除频率最低,且最久没有访问的结点
DLHead dlHead = head.next;
Value delValue = dlHead.head.next;
dlHead.head.next = delValue.next;
dlHead.head.next.pre = dlHead.head;
cache.remove(delValue.key);
dlHead.length--;
if(dlHead.length == 0){
// 删掉这个dlHead
dlHead.pre.next = dlHead.next;
dlHead.next.pre = dlHead.pre;
freq.remove(dlHead.frequency);
}
}
// 新插入节点
Value insertValue = new Value(key,value);
cache.put(key,insertValue);
if(!freq.containsKey(insertValue.frequency)){
// 没有这个频率的, 即需要在头后面插入
DLHead newHead = new DLHead(insertValue.frequency);
newHead.next = head.next;
newHead.next.pre = newHead;
head.next = newHead;
newHead.pre = head;
freq.put(insertValue.frequency,newHead);
}
DLHead newHead = freq.get(insertValue.frequency);
newHead.tail.pre.next = insertValue;
insertValue.pre = newHead.tail.pre;
insertValue.next = newHead.tail;
newHead.tail.pre = insertValue;
newHead.length++;
}
}
private void updateFreq(int key){
// 找到结点
Value tmpValue = cache.get(key);
DLHead tmpHead = freq.get(tmpValue.frequency);
// 更新结点的频度
tmpValue.frequency++;
// 将结点与竖链断链
tmpValue.next.pre = tmpValue.pre;
tmpValue.pre.next = tmpValue.next;
// 更新tmpHead的长度
tmpHead.length--;
int newFreqVal = tmpValue.frequency;
if(!freq.containsKey(newFreqVal)){
// 没有这个频率,需要增加dlhead.
DLHead newDLHead = new DLHead(tmpValue.frequency);
newDLHead.next = tmpHead.next;
newDLHead.pre = tmpHead;
tmpHead.next = newDLHead;
newDLHead.next.pre = newDLHead;
freq.put(tmpValue.frequency,newDLHead);
}
if(tmpHead.length == 0){
// 删掉tmpHead
tmpHead.pre.next = tmpHead.next;
tmpHead.next.pre = tmpHead.pre;
freq.remove(tmpHead.frequency);
}
tmpHead = freq.get(tmpValue.frequency);
tmpValue.pre = tmpHead.tail.pre;
tmpValue.next = tmpHead.tail;
tmpValue.pre.next = tmpValue;
tmpHead.tail.pre = tmpValue;
tmpHead.length++;
}
}