LRU缓存机制
题目描述
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。进阶:你是否可以在
O(1)
时间复杂度内完成这两种操作?
示例
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
1:LRU缓存算法
LRU缓存算法
就是一种缓存淘汰策略。LRU
全称Least Recently Used
。计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。那么,什么样的数据,我们判定为「有用的」的数据呢?
-
打开手机,后台多开几个应用,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6v2QRMK-1592736842267)(http://myblogoss.aimezhao.online/20200525214218.png)]
-
切换到任务视图界面,你会发现几个,然后你点击设置,再次打开的情况下设置就到了第一个
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8ynqV0j-1592736842269)(http://myblogoss.aimezhao.online/20200525214304.png)]
- 此时,设置就最近使用的,而手机管家是最久未使用的,当缓存容量不够的时候,就要先把最久未使用的手机管家给淘汰掉。这就是
LRU
的淘汰策略。
- 此时,设置就最近使用的,而手机管家是最久未使用的,当缓存容量不够的时候,就要先把最久未使用的手机管家给淘汰掉。这就是
2:LinkedHashmap
仅仅作为了解,请记住这是一个算法题。
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
3:哈希表+队列
- 利用一个哈希表和一个队列维护所有的键值对。
- 哈希表为普通的哈希映射
(HashMap)
,用来记录缓存数据的键和值。 - 队列用来判断当前的缓存内是否存在密钥
(key)
,既然是队列,那么就是队尾插入,所以队头的是最久未使用的,队尾的是最近使用的
- 哈希表为普通的哈希映射
- 对于
get
操作,首先用队列判断密钥(key)
是否存在key
不存在,返回-1
key
存在,将队列中密钥为key的先删除,后加入(加入时密钥为key的数据在队列的队头)
- 对于
put
操作,首先判断密钥(key)
是否存在key
不存在,将次缓存数据的key和value加入到HashMap
中。然后判断此时的缓存容量是否超容,超容的情况下,弹出map中key为队列中最后的那一个keykey
存在,更新队列中此key的位置到队头,更新HashMap
中此key对应的value
class LRUCache{
private int capacity; //缓存容量
Queue<Integer> queue;
Map<Integer, Integer> map;
public LRUCache(int capacity) {
this.map = new HashMap<>();
this.queue = new LinkedList<>();
this.capacity = capacity;
}
// get操作
public int get(int key) {
if(queue.contains(key)) {
queue.remove(key);
queue.add(key);
return map.get(key);
} else {
return -1;
}
}
public void put(int key, int value) {
if(queue.contains(key)) {
queue.remove(key);
queue.add(key);
map.put(key, value);
} else if(capacity==0) {
// 如果缓存容量为0,弹出最后队头元素
map.remove(queue.poll());
queue.add(key);
map.put(key, value);
} else {
queue.add(key);
map.put(key, value);
capacity--;
}
}
}
4:哈希表+双向链表
这个方法才算是这个算法题真正想要考察的地方,手写
LRU
- 利用一个哈希表和一个双向链表维护所有的键值对。
- 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
- 哈希表即为普通的哈希映射
(HashMap)
,通过缓存数据的键映射到其在双向链表中的位置。
get
操作- 如果
key
不存在,返回-1
key
存在,通过哈希表HashMap
找到该缓存项在双向链表中的位置,然后移动到双向链表的头部,最后返回该节点的值
- 如果
put
操作- 哈希表
HashMap
中不存在该key
,将该key
和value
加入到哈希表中,并创建一个新节点添加到双向链表的头部 - 哈希表中存在该
key
,更新该key
对应的value
,将此节点移动到双向链表头部 - 特殊情况:判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项。
- 哈希表
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
时间复杂度:对于
put
和get
都是O(1)
,空间复杂度:O(capacity)