LRU缓存
146.LRU缓存篇
问题描述
实现一个满足LRU(最近最少使用)缓存约束的LRUCache类。该缓存满足以下三个条件:
- 缓存容量
LRUCache(int capacity)
int get(int key)
如果关键字key存在,则返回对应value,否则返回-1;void put(int key, int value)
如果关键字存在,更新value的值;如果不存在,则向缓存中插入新键值对,如果插入导致超过缓存容量,则删除最近最少使用的关键字的键值对。
要求:get 与 put要以O(1)实现
思路:
- get与put要以O(1)实现 => 数组
- 键值对 => map
1+2 = HashMap(底层就是数组加链表加红黑树)
但是在put新键值对时,要删除最近最少使用的键值对,同时put要以O(1)实现,这就要求我们在存储数据时要求它有序存储,实现O(1)复杂度找到要删除的节点,这里很自然过渡到了LinkedHashMap,因为他就是在HashMap的基础上维护了双向链表来实现有序存储。
问题解决
- 方法1:直接利用LinkedHashMap来解决,这要求了解其源码
class LRUCache extends LinkedHashMap<Integer, Integer> {
private static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容因子,当实际容量与最大容量比值大于扩容因子时进行扩容。
private final int capacity;//容量
public LRUCache(int capacity) {
super(capacity, DEFAULT_LOAD_FACTOR, true);//这里的true是初始化accessOrder为true:即按照读取顺序来排序,默认是false:插入顺序排序
this.capacity = capacity;//容量
}
public int get(int key) {
return super.getOrDefault(key, -1);//这里调用的getOrDefault就是去判断是否有这个key,没有就返回-1
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;//指定当map的容量大于指定容量时执行删除最老的元素(这个最老就是最近最少使用:通过指定accessOrder实现)
}
}
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);//调用的这个函数是执行读取操作后的回调函数,里面实现了将读的元素放到末尾
return e.value;
}
- 方法2:自己通过双向链表与HashMap来实现,其中HashMap为key to node的映射
class LRUCache { private static class LinkedListNode { int key, value; //键值对 LinkedListNode pre, next; //双向链表 public LinkedListNode(int key, int value) { this.key = key; this.value = value; } } //利用dummy简化代码逻辑,将双向链表转换为双向循环链表,减少判空 private final LinkedListNode dummy = new LinkedListNode(0, 0); //缓存的容量 private final int capacity; //存储key到节点的映射 private final Map<Integer, LinkedListNode> hashmap = new HashMap<>(); public LRUCache(int capacity) { //初始化 this.capacity = capacity; //一开始就让他自循环,就不用再插入,移动操作中判空了,没有dummy节点的话可以参考LinkedHashMap的源码,定义一个指针head指向头部,一个指针指向尾部tail dummy.next = dummy; dummy.pre = dummy; } public int get(int key) { if (hashmap.containsKey(key)) { //表中已经存在关键字,除了取之外还要调整他在链表中的位置 LinkedListNode node = hashmap.get(key); //删除该节点重新从头插入,以下都是通过这样实现位置排序的 delete(node); insert(node); return node.value; } else { //表中没有关键字 return -1; } } public void put(int key, int value) { if (hashmap.containsKey(key)) { //表中存在,更改值,并更新链表 LinkedListNode node = hashmap.get(key); node.value = value; delete(node); insert(node); } else { //表中不存在,插入key-value,插入时判断是否超过了容量 LinkedListNode newNode = new LinkedListNode(key, value); if (hashmap.size() == capacity) { //需要删除最近少使用的节点:尾巴节点,删除一个节点包括删除他的hash表中的键值对 LinkedListNode node = dummy.pre; hashmap.remove(node.key); delete(node); //插入新节点 insert(newNode); hashmap.put(key, newNode); } else { //直接插入 hashmap.put(key, newNode); //从头插入链表 insert(newNode); } } } private void delete(LinkedListNode node) { node.pre.next = node.next; node.next.pre = node.pre; } private void insert(LinkedListNode node) { node.pre = dummy; node.next = dummy.next; node.pre.next = node; node.next.pre = node; }
}
tips:
- 涉及到链表与hash表两个我们维护的表时,也就是多表需要程序员维护时,想清楚什么时候要操作什么表。
- dummy node 这里的作用是简化代码逻辑,dummy node还可以应用头节点改变时,找到头节点,dummy.next
- 通过源码阅读,了解了怎么优雅的操作双向链表的插入,删除。