题目介绍
一个缓存结构需要实现如下功能。
set(key, value):将记录(key, value)插入该结构
get(key):返回key对应的value值
但是缓存结构中最多放K条记录,如果新的第K+1条记录要加入,就需要根据策略删掉一条记录,然后才能把新记录加入。这个策略为:在缓存结构的K条记录中,哪一个key从进入缓存结构的时刻开始,被调用set或者get的次数最少,就删掉这个key的记录;
如果调用次数最少的key有多个,上次调用发生最早的key被删除
这就是LFU缓存替换算法。
概念描述
简单来说,LFU缓存结构就是把数据按照访问的次数来进行排序,当缓存满的时候,则需要根据策略进行淘汰。
- 正常情况下,直接删除访问次数最少的那个数据;
- 当最少访问次数有多个不同的数据时,则删除最早没被访问的数据。
要想实现LFU缓存,其实并没有那么简单,需要注意的点很多,所以我们要想在面试过程中书写出来一个没有BUG的LFU缓存结构,就需要有一定的实现方向。从整体出发,再到具体实现。
那么现在就来带你一步一步实现LFU缓存结构。
算法实现
定义
先提取出我们需要注意的功能点:
1、容量有限。
2、需要通过key去访问。
3、对key操作时,key的访问次数会增加。
4、需要淘汰访问次数最少的那个key(有多个则淘汰掉最早没被访问的)。
5、题目要求 set和get方法的时间复杂度为O(1)
。
所以,我们就可以根据上面的条件来定义出一些用到的属性了。
int capacity; //限制缓存的最大容量 HashMap<Integer,Integer> kv; //能够通过key获取到val HashMap<Integer,Integer> kt; //能够通过key获取访问的次数times /* 我们看到上面提出来的功能点4。 我们需要淘汰times做少的key,所以我们需要有times到key的映射 HashMap<Integer,Integer> tk; 但是会有多个key的访问次数相同,并且此时我们需要淘汰那个最先放进去缓存中的key, 这里我们可以利用LinkedHashSet,它会根据我们put进去的数据进行时间的排序。 所以为了兼顾这两者我们定义出下面的属性tk。 我们可以通过访问的次数times拿到所有访问次数为times的key的集合,这些key则会按照放进去的时间先后顺序存储,这就满足了我们的淘汰策略。 */ HashMap<Integer,LinkedHashSet<Integer>> tk; // 我们还需要记录最小的访问次数times,方便淘汰此略的使用 int minTimes;
那么,现在我们就可以来写出这个类了。
class LFUCache{ int capacity; // 容量 HashMap<Integer,Integer> kv; //能够通过key获取到val HashMap<Integer,Integer> kt; //能够通过key获取访问的次数times HashMap<Integer,LinkedHashSet<Integer>> tk; //能够通过times获取key int minTimes; // 记录访问的最少次数 } public LFUCache(int capacity){ this.kv= new HashMap<>(); this.kt= new HashMap<>(); this.tk= new HashMap<>(); this.minTimes= 0; //刚开始的次数为0 this.capacity= capacity; }
我们一个简单的LFU结构肯定有两个必不可少的方法,get
and set
public int get(int key){ } public void set(int key,int value){ }
get
根据需求 get方法的流程大概如下图
那么现在我们就来根据流程实现
public int get(int key){ if(!kv.containsKey(key)){ return -1; } // 该key访问次数要加1 increaseTimes(key); return kv.get(key); }
至于increaseTimes方法的实现,我们先不着急,等总体的框架先形成了,再处理细节。
set
还是一样,我们先把put的流程画出来
那么还是一样根据流程图来实现代码
public void set(int key,int value){ // 容量不合法 if(this.capacity <= 0) return; /* key存在 */ // 1.覆盖value if(kv.containsKey(key)){ kv.put(key,value); //2.访问次数+1 increaseTimes(key); return; } /* key不存在 */ // 1、cache满了 if(this.capacity <= kv.size()){ // 淘汰Times最小的 removeMinTimes(); } // 2、cache没满 // 先插入kv表 kv.put(key,value); // kt表 首次插入,访问次数为1 kt.put(key,1); // 下面步骤为淘汰策略做准备 // 插入tk表 // 当times为1的所有的key的集合不存在时,就新建一个 tk.putIfAbsent(1,new LinkedHashSet<Integer>()); // 否则,则直接插入 tk.get(1).add(key); // 插入后此时最小的times肯定就是1了 this.minTimes = 1; }
好了,现在我们整个LFU缓存结构的雏形已经形成了,基本的get和set方法都已经有了,那么我们就来实现具体的方法了。
increaseTimes(int key)
这个方法是LFU中的核心方法,是将key的访问次数+1。
我们将所需要涉及到的集合先列出来:
- kt表。需要将原来的key,times的映射改为key,times+1 。
- tk表。需要将key从原来的times,set中去除,加入到times+1,set中去。(这里的set表示,访问次数为times的所有的key的集合)。
所以有了这波分析,还是一样,我们先来画流程图
private void increaseTimes(int key){ // 拿到原来的times int times = kt.get(key); // 更新kt表 kt.put(key,times+1); // 更新tk表 // 将key从原来的times,set中去除 tk.get(times).remove(key); // 将key加入到times+1,set中去 // 但是还需要考虑times+1对应的set集合是否存在 tk.putIfAbsent(times+1,new LinkedHashSet<Integer>()); tk.get(times+1).add(key); // 此时基本已经完成了,但是我们还需要做一些细节处理 // 当我们从原来的set集合中移除的时候,我们得考虑一下这个key是否为其最后一个 if(tk.get(times).isEmpty()){ // 如果是的话,则我们需要销毁对应的set集合 tk.remove(times); // 如果times刚好为最小的,则需要更新minTimes if(times == this.minTimes){ this.minTimes++; } } }
写到这里,是不是觉得离我们的目标越来越近,而且整个过程也很清晰?那么下面我们就来实现最后一个核心方法!
removeMinTimes()
在一开始我们就有说到淘汰的策略,主要分为两种情况:
1、当最小的times中,只有一个key时,则直接淘汰
2、否则,则需要淘汰最早没被访问的key
这个方法其实实现起来不难,难点是在原来设计用什么数据结构来解决我们的难点。
所以我们直接实现代码:
private void removeMinTimes(){ // 获取最小的times的set集合 LinkedHashSet<Integer> set = tk.get(this.minTimes); // 这里是直接将两个步骤结合起来,利用了LinkedHashSet这个数据结构底层的设计 // LinkedHashSet底层是按照key放进去的时间先后进行排序的 // 所以此时set集合中的第一个即为需要删除的key int delKey = set.iterator().next(); // 移除掉 set.remove(delKey); if(set.isEmpty()){ tk.remove(this.minTimes); } // 更新kv表 kv.remove(delKey); // 更新tk表 tk.remove(delKey); }
代码很简单,就是将这个key从对应的set中移除掉,然后再在kv和tk表中移除掉即可。
但是,细心的人应该会发现,我们将minTimes对应的key移除掉,如果这个key刚好只有一个,没有多个,我们的minTimes不是需要更新了吗?(tips: minTimes为最小的访问次数)
如果发现了这一点的同学,确实考虑得很细节,正常情况来说,肯定是需要更新这个值的,
但是,我们来看一下 removeMinTimes()
这个方法是在什么时候调用的?在put进去新的 k,v 键值对的之后,cache满了我们才需要去进行removeMinTimes。注意,put进去的是新的,所以我们在put方法里面,已经将这个minTimes更新为1了,是不是很细节哈哈哈。
到这里,我们整个LFU缓存结构的设计就已经结束了,我们只需要将上面的方法拼在一起,则可以得到一个完成的LFU了。
END
给出这道题目的代码
import java.util.*; public class Solution { /** * lfu design * @param operators int整型二维数组 ops * @param k int整型 the k * @return int整型一维数组 */ public int[] LFU (int[][] operators, int k) { // write code here LRUCache lru = new LRUCache(k); int len = 0; for(int i = 0; i < operators.length; i++){ if(operators[i][0] == 2) len++; } // 返回数组 int[] res = new int[len]; len = 0; for(int i = 0; i < operators.length; i++){ if(operators[i][0] == 1){ lru.set(operators[i][1],operators[i][2]); }else{ res[len++] = lru.get(operators[i][1]); } } return res; } } class LRUCache{ int capacity; HashMap<Integer,Integer> kv; HashMap<Integer,Integer> kt; HashMap<Integer,LinkedHashSet<Integer>> tk; int minTimes; public LRUCache(int capacity){ this.kv= new HashMap<>(); this.kt= new HashMap<>(); this.tk= new HashMap<>(); this.minTimes= 0; //刚开始的次数为0 this.capacity= capacity; } public int get(int key){ if(!kv.containsKey(key)){ return -1; } // 该key访问次数要加1 increaseTimes(key); return kv.get(key); } public void set(int key,int value){ // 容量不合法 if(this.capacity <= 0) return; /* key存在 */ // 1.覆盖value if(kv.containsKey(key)){ kv.put(key,value); //2.访问次数+1 increaseTimes(key); return; } /* key不存在 */ // 1、cache满了 if(this.capacity <= kv.size()){ // 淘汰Times最小的 removeMinTimes(); } // 2、cache没满 // 先插入kv表 kv.put(key,value); // kt表 首次插入,访问次数为1 kt.put(key,1); // 插入tk表 // 当times为1的所有的key的集合不存在时,就新建一个 tk.putIfAbsent(1,new LinkedHashSet<Integer>()); // 否则,则直接插入 tk.get(1).add(key); // 插入后此时最小的times肯定就是1了 this.minTimes = 1; } private void increaseTimes(int key){ // 拿到原来的times int times = kt.get(key); // 更新kt表 kt.put(key,times+1); // 更新tk表 // 将key从原来的times,set中去除 tk.get(times).remove(key); // 将key加入到times+1,set中去 // 但是还需要考虑times+1对应的set集合是否存在 tk.putIfAbsent(times+1,new LinkedHashSet<Integer>()); tk.get(times+1).add(key); // 此时基本已经完成了,但是我们还需要做一些细节处理 // 当我们从原来的set集合中移除的时候,我们得考虑一下这个key是否为其最后一个 if(tk.get(times).isEmpty()){ // 如果是的话,则我们需要销毁对应的set集合 tk.remove(times); // 如果times刚好为最小的,则需要更新minTimes if(times == this.minTimes){ this.minTimes++; } } } private void removeMinTimes(){ // 获取最小的times的set集合 LinkedHashSet<Integer> set = tk.get(this.minTimes); // 这里是直接将两个步骤结合起来,利用了LinkedHashSet这个数据结构底层的设计 // LinkedHashSet底层是按照key放进去的时间先后进行排序的 // 所以此时set集合中的第一个即为需要删除的key int delKey = set.iterator().next(); // 移除掉 set.remove(delKey); if(set.isEmpty()){ tk.remove(this.minTimes); } // 更新kv表 kv.remove(delKey); // 更新tk表 tk.remove(delKey); } }
复杂度分析:
LFU缓存结构的set和get都满足
时间复杂度:。for循环的次数
空间复杂度:。返回数组的空间大小。