1 题目链接
2 题目要求
描述
设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
- set(key, value):将记录(key, value)插入该结构
- get(key):返回key对应的value值
要求
- set和get方法的时间复杂度为O(1)
- 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。
- 当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
若opt=1,接下来两个整数x, y,表示set(x, y)
若opt=2,接下来一个整数x,表示get(x),若x未出现过或已被移除,则返回-1
对于每个操作2,输出一个答案
示例
输入:[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
返回值:[1,-1]
说明:
第一次操作后:最常使用的记录为("1", 1)
第二次操作后:最常使用的记录为("2", 2),("1", 1)变为最不常用的
第三次操作后:最常使用的记录为("3", 2),("1", 1)还是最不常用的
第四次操作后:最常用的记录为("1", 1),("2", 2)变为最不常用的
第五次操作后:大小超过了3,所以移除此时最不常使用的记录("2", 2),加入记录("4", 4),并且为最常使用的记录,然后("3", 2)变为最不常使用的记录
3 代码思路
读完题目之后,注意到set和get的时间复杂度都为O(1),因此首先想到的是使用Map结构。接着,LRU的读取和写入操作都需要将元素设置为最常使用,其中写入操作当元素个数大于等于容量k时,还需要将最不经常使用的元素从图中移除。因此,我们想到需要将数组按照使用时间的先后顺序排序。结合上述两点:Map结构 + 时间有序,想到使用 LinkedHashMap
来解决这道题。
遍历所给的二维数组,根据每个数组的第一个字段判断是set还是get。如果是set,需要判断元素总数和容量k的关系,容量小于k时,将元素加入map,大于等于k时,移除链表中的第一个元素(即最不常用的元素)。如果是get,map中存在key时,将其对应的值返回,并把元素从map中删除,重新加入链表。否则返回-1。
4 代码实现
public class Solution {
public int[] LRU (int[][] operators, int k) {
Map<Integer,Integer>map = new LinkedHashMap();
List<Integer>list = new ArrayList(); // 在这里并不知道一共执行多少次get,使用列表代替数组存储get的值。
for(int i = 0;i < operators.length; i++){
int opt = operators[i][0];
int key = operators[i][1];
switch(opt){ // opt==1,执行set;opt==2,执行get
case 1:
if(map.size() >= k){ // 判断元素总数是否超过容量K
Iterator it = map.keySet().iterator();
map.remove(it.next()); // 移除第一个元素
}
map.put(key,operators[i][2]);
break;
case 2:
if(map.containsKey(key)){ // 判断map是否有当前key,存在则返回,不存在则返回-1
int val = map.get(key);
list.add(val);
map.remove(key);
map.put(key,val);
}else{
list.add(-1);
}
break;
}
}
int []result = new int[list.size()];
for(int i = 0; i < list.size(); i++){ // 遍历列表,存入数组
result[i] = list.get(i);
}
return result;
}
}
5 知识扩充
什么是LRU?
LRU英文全称Least Recently Used,可以理解为最久没有使用。在这种策略下我们用最近一次使用的时间来衡量一块内存的价值,越久之前使用的价值也就越低,最近刚刚使用过的,后面接着会用到的概率也就越大,那么自然也就价值越高。
当然只有这个限制是不够的,我们前面也说了,由于内存是非常金贵的,导致我们可以存储在缓存当中的数据是有限的。比如说我们固定只能存储1w条,当内存满了之后,缓存每插入一条新数据,都要抛弃一条最长没有使用的旧数据。这样我们就保证了缓存当中的数据的价值都比较高,并且内存不会超过限制。
我们把上面的内容整理一下,可以得到几点要求:
- 保证缓存的读写效率,比如读写的复杂度都是O(1)
- 当一条缓存当中的数据被读取,将它最近使用的时间更新
- 当插入一条新数据的时候,弹出更新时间最远的数据
LRU原理
我们仔细想一下这个问题会发现好像没有那么简单,显然我们不能通过数组来实现这个缓存。因为数组的查询速度是很慢的,不可能做到O(1)。其次我们用 HashMap
好像也不行,因为虽然查询的速度可以做到O(1),但是我们没办法做到更新最近使用的时间,并且快速找出最远更新的数据。
如果是在面试当中被问到想到这里的时候,可能很多人都已经束手无策了。但是先别着急,我们冷静下来想想会发现问题其实并没有那么模糊。首先 HashMap是一定要用的,因为只有 HashMap
才可以做到O(1)时间内的读写,其他的数据结构几乎都不可行。但是只有 HashMap
解决不了更新以及淘汰的问题,必须要配合其他数据结构进行。这个数据结构需要能够做到快速地插入和删除,其实我这么一说已经很明显了,只有一个数据结构可以做到,就是链表。
链表有一个问题是我们想要查询链表当中的某一个节点需要O(n)的时间,这也是我们无法接受的。但这个问题并非无法解决,实际上解决也很简单,我们只需要把链表当中的节点作为 HashMap
中的value进行储存即可,最后得到的系统架构如下: