本文为个人解题思路整理,水平有限,有问题欢迎交流
概览
这题有两个解决方案,第二个的性能比第一个稍强,但是建议练习第一种方法,当然实际使用中性能优先
难度:中等
核心知识点:自定义链表 + map
题目来源
牛客:https://www.nowcoder.com/practice/e3769a5f49894d49b871c09cadd13a61
力扣:https://leetcode-cn.com/problems/lru-cache-lcci/
本文依照牛客的要求解答,力扣的解决方案也是一样的
题目内容
设计LRU缓存结构,其大小在构造时确定
需求功能如下:
- set(key, value):将记录(key, value)插入该结构
- get(key):返回key对应的value值
额外要求如下:
- set和get方法的时间复杂度为O(1)
- 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的。
- 当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的。
操作模式:
- [1,x,y]:表示
set(x,y)
,即存储[x,y],若结构的长度超出上限,则将最久远的移除 - [2,x]:表示
get(x)
,即获取x对应值,若不存在则返回-1
样例
数据源1
[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
输出1
[1,-1]
数据源2
[[1,1,1],[1,2,2],[1,3,3],[1,4,4],[2,4],[2,3],[2,2],[2,1],[1,5,5],[2,1],[2,2],[2,3],[2,4],[2,5]],3
输出2
[4,3,2,-1,-1,2,3,-1,5]
方法1
解题思路
-
先整理需求
- 需要以O(1)的复杂度进行增删改查
- 容器内的数据需要根据活跃度排序,一旦数据被使用那么需要将其调整到队首
- 当容器内数据超出上限的时候踢出最不活跃的一个
-
思考
- O(1)的增删改查,首先想要的是map,但显然仅仅使用map不够完成其他需求
- 将某个数据插入队首,显然队列或者链表比较方便完成,但是队列不方便操作队列中间的元素,那么只能使用链表了
- 需要对容器设置上限,第一想法是队列,用不了,第二想法是使用map检查数量,但是怎么确定谁最久远呢
基本确定下来使用map+链表
-
整理思路
- 使用map保存数据的key,并通过
map.size()
检查数据数量 - 使用链表存储数据,新数据放在队首,在数据被使用时也将其挪到队首,那么队尾的就是最不活跃的那个了
- map的value保存对链表元素的映射,那么即可在O(1)的复杂度下完成查找到数据,且链表的增删改都是O(1)的复杂度
- 使用map保存数据的key,并通过
解题方案
定义一个节点类Node:保存四个数据:key,value,上个节点pre,下个节点next
构造一个链表:头为节点head,尾为节点tail,当然初识两个都为null
定义map:key为数据的key,value为key对应的节点
-
set(x,y):
- 检查x在map中是否存在,若存在则可确定其节点,将其从map和链表中一起删除
- 构造一个节点
Node(x,y)
,将其添加到链表的头部,并将(x,node)
存储到map中,这个节点即最活跃的节点 - 检查map的大小是否超出限制,若超出限制则直接获取链表的尾结点,即最不活跃的元素
- 尾节点的key即map中的key,即可以确定map中的目标
- 删除map中的该节点,直接remove掉即可
- 删除链表中的尾结点,将尾结点向前挪一位即可
-
get(x):
- 检查x在map中是否存在
- 存在
- 通过map即可确定节点位置
- 保存节点的字段value作为结果(但先不return)
- 将节点从链表中移除
- 将节点重新添加至链表头部
- 不存在:保存-1作为结果即可
- 存在
- 检查x在map中是否存在
完整代码
public class Lru {
public static void main(String[] args) {
// write your code here
Lru lru = new Lru();
}
public Lru() {
int[][] operators = new int[][]{
{1, 1, 1},
{1, 2, 2},
{1, 3, 3},
{1, 4, 4},
{2, 4},
{2, 3},
{2, 2},
{2, 1},
{1, 5, 5},
{2, 1},
{2, 2},
{2, 3},
{2, 4},
{2, 5}
};
int[] ans = LRU(operators, 3);
for (int i : ans) {
System.out.println(i);
}
}
Node head, tail;//链表的头尾,用于充当队列
Map map;//用于存储key与节点的映射
int maxSize;//最大长度,全局变量
List ansList;//答案列表
public int[] LRU(int[][] operators, int k) {
maxSize = k;
head = tail = null;
map = new HashMap<>();
ansList = new ArrayList<>();
for (int i = 0; i < operators.length; i++) {
if (operators[i][0] == 1) {
put(operators[i][1], operators[i][2]);
} else if (operators[i][0] == 2) {
get(operators[i][1]);
}
}
return ansList.stream().mapToInt(m -> m.intValue()).toArray();
}
void put(int key, int value) {
Node newHead;
if (map.containsKey(key)) {
//包括该节点,则获取该节点,并从链表中删除
newHead = map.get(key);
remove(newHead);
}
//重建节点,并重新放置于链表头部
newHead = new Node(key, value);
map.put(key, newHead);
//将最新的节点添加到队列头部
push(newHead);
//超出上限,则移除最后一位
if (map.size() > maxSize) {
pop();
}
}
void get(int key) {
if (map.containsKey(key)) {
//包括该节点,则获取节点
ansList.add(map.get(key).val);
//移除节点,添加到头部
remove(map.get(key));
push(map.get(key));
} else {
//不包括节点,返回-1
ansList.add(-1);
}
}
void remove(Node node) {
//将首位相连,那么自己将被孤立出去
//这里要特殊处理一下pre或next为空的情况
Node pre = node.pre;
Node next = node.next;
if (head == tail) {
//若本就只有一个节点,则清空队列
head = null;
tail = null;
} else {
//若当前节点为首节点,则首节点指向next
if (head != node) {
pre.next = next;
} else {
head = next;
}
//若当前节点为尾结点,则尾结点指向pre
if (tail != node) {
next.pre = pre;
} else {
tail = pre;
}
}
}
void push(Node node) {
//将自己添加到头部
//特殊处理当前为空的情况
if (head == null) {
head = tail = node;
} else {
node.pre = null;
node.next = head;
head.pre = node;
head = node;
}
}
void pop() {
//将最后一位从map中移除
map.remove(tail.key);
//tail向前挪一位,此时tail有可能变为null
tail = tail.pre;
}
/**
* 自定义节点,用于构造链表
*/
class Node {
int key, val;
Node pre, next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
}
性能
记得提交代码时删除打印结果,会影响性能
方法2
解题思路
需求如方法1一样,不再赘述
-
思考
- 需要O(1)完成增删改查,那么肯定是map啦
- 需要对数据进行排序,那么可以用跟方法1同样的处理,即被删除使用的节点,再重新添加至头部
- 但是map没有头尾?其实有的,LinkedHashMap,也是map的一种,但是可以保留顺序,最先放入在头部,最后放入的在尾部,使用他的迭代器iterator可以从头开始迭代
-
整理思路
- 使用LinkedHashMap存储数据
- 如果数据变动,那么将数据删除掉,重新添加即可
- 当数据超过上限的时候,将第一个数据,即最不活跃的数据删掉
这样整理后,最活跃的将在最末尾,最不活跃的在最前面,O(1)的时间即可删除掉
解题方案
建立LinkedHashMap用于存储数据
-
set(x,y):
- 检查是否存在key为x的数据,若有则删除掉
- 添加数据
(x,y)
到末尾 - 检查数据是否超出上限,若超出上限则将第一个数据删除掉
-
get(x):检查是否存在key为x的数据
- 若有,获取这个数据,并将其删除,再将其添加到哈希表中
- 若没有,则返回-1
完整代码
public class Lru2 {
public static void main(String[] args) {
// write your code here
Lru2 lru = new Lru2();
}
public Lru2() {
int[][] operators = new int[][]{
{1, 1, 1},
{1, 2, 2},
{1, 3, 3},
{1, 4, 4},
{2, 4},
{2, 3},
{2, 2},
{2, 1},
{1, 5, 5},
{2, 1},
{2, 2},
{2, 3},
{2, 4},
{2, 5}
};
int[] ans = LRU(operators, 3);
// for (int i : ans) {
// System.out.println(i);
// }
}
LinkedHashMap map;//用于存储key与节点的映射
int maxSize;//最大长度,全局变量
List ansList;//答案列表
public int[] LRU(int[][] operators, int k) {
//初始化
maxSize = k;
map = new LinkedHashMap<>();
ansList = new ArrayList<>();
//开始执行业务
for (int i = 0; i < operators.length; i++) {
if (operators[i][0] == 1) {
put(operators[i][1], operators[i][2]);
} else if (operators[i][0] == 2) {
get(operators[i][1]);
}
}
//结果从list转为数组
return ansList.stream().mapToInt(m -> m.intValue()).toArray();
}
void put(int key, int value) {
if (map.containsKey(key)) {
//包括该节点,则获取该节点,并从链表中删除
map.remove(key);
}
//重建节点,并重新放置于链表头部
map.put(key, value);
//将最新的节点添加到队列头部
//超出上限,则移除最后一位
if (map.size() > maxSize) {
map.remove(map.entrySet().iterator().next().getKey());
}
}
void get(int key) {
int value = -1;
if (map.containsKey(key)) {
//包括该节点,则获取节点
value = map.get(key);
//移除节点,添加到头部
map.remove(key);
map.put(key, value);
}
ansList.add(value);
}
}
执行结果
性能
提交代码记得关闭打印哟
比较
- 方法1使用了传统的map和链表,两者互补,从而满足业务需求
- 方法2直接使用LinkedHashMap即可满足所有需求
- 方法2性能稍微由于方法1,但两者差距不大
后记
不难看出其实方法1应该才是出题人想要考察的,然而LinkedHashMap直接满足了所有需求。。。
所以建议按照方法1学习和理解这题,但实际应用中当然性能优先,学操作是好事,秀操作可不是
作者:Echo_Ye
WX:Echo_YeZ
Email :[email protected]
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)