问题描述
题目描述
设计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],[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)变为最不常使用的记录
解决思路
分析
- 借助Java的HashMap,这样不需要考虑存储的问题,我们只要做通过链表实现lru(最近最少使用)即可。
- 不实用Java的HashMap,实现原声的hash存储(后期补充)
代码实现
// 思路1
public class Solution {
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
public int[] LRU (int[][] operators, int k) throws Exception {
LRU<Integer, Integer> lru = new LRU<>(k);
List<Integer> list = new ArrayList<>();
// write code here
for (int i = 0; i < operators.length; i++) {
int[] operator = operators[i];
if (operator[0] == 1) {
lru.put(operator[1], operator[2]);
} else if (operator[0] == 2) {
Integer temp = lru.get(operator[1]);
if (temp == null) {
list.add(-1);
} else {
list.add(temp);
}
}
}
int[] result = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
}
// 重新考虑,LRU:最近最少使用
// 数据结构:链表(可以通过现有的,也可以通过自定义的node节点)、map集合
// 方法:LRU:put方法,get方法;
// addFirst,添加到头部节点
// remove,移除节点
class LRU<K, V> {
// 头尾节点作为空节点
private Node head = new Node();
private Node tail = new Node();
// 记录K-Node映射,便于快速查找目标数据对应节点
private HashMap<K, Node> map;
private int maxSize;
// 哈希链表节点类 Node
private class Node {
Node pre;
Node next;
K k;
V v;
public Node() {
}
// Node对外提供构造方法
public Node(K k, V v) {
this.k = k;
this.v = v;
}
}
// 初始化时必须传入最大可用内存容量
public LRU(int maxSize) {
this.maxSize = maxSize;
// HashMap初始容量设置为 maxSize * 4/3,即达到最大可用内存时,HashMap也不会自动扩容浪费空间
// 通过反向计算,可以避免map的扩容
this.map = new HashMap<>(maxSize * 4 / 3);
// 设置头节点的后置节点和尾节点的前置节点
head.next = tail;
tail.pre = head;
}
// 获取指定数据
public V get(K key) {
// 判断是否存在对应数据
if (!map.containsKey(key)) {
return null;
}
// 最新访问的数据移动到链表头,从链表中移除节点并追加到尾部
Node node = map.get(key);
remove(node);
addFirst(node);
return node.v;
}
// 更新旧数据或添加数据
public void put(K key, V value) {
System.out.println("K:" + key + ";V="+value);
// 若存在旧数据则删除
if (map.containsKey(key)) {
Node node = map.get(key);
remove(node);
}
// 新数据对应节点插入链表头
Node node = new Node(key, value);
map.put(key, node);
addFirst(node);
// 判断是否需要淘汰数据
if (map.size() > maxSize) {
node = removeLast();
// 数据节点淘汰后,同时删除map中的映射
map.remove(node.k);
}
}
// 将指定节点插入链表头
private void addFirst(Node node) {
Node next = head.next;
// 设置追加的节点为当前节点的后置节点
head.next = node;
// 设置当前节点的前置节点为头节点
node.pre = head;
node.next = next;
next.pre = node;
}
// 从链表中删除指定节点
private void remove(Node node) {
Node pre = node.pre;
Node next = node.next;
// 跳过当前节点
pre.next = next;
// 设置下一个节点的前置节点
next.pre = pre;
node.next = null;
node.pre = null;
}
// 淘汰数据
private Node removeLast() {
// 找到最近最久未使用的数据所对应节点
Node node = tail.pre;
// 淘汰该节点
remove(node);
return node;
}
}
小伙伴如果想测试的话,可以直接到牛客网这个链接做测试