18, 146. LRU 缓存
https://leetcode-cn.com/problems/lru-cache/
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。
实现 LRUCache 类:
- LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
思路1: 通过继承java中linkedHashmap直接实现
package com.shangguigu.dachang.algrithm.A06_hashTable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author : 不二
* @date : 2022/4/14-下午9:16
* @desc : 146. LRU 缓存
* https://leetcode-cn.com/problems/lru-cache/
*
**/
public class A63_LRUCacheWithLinkedHashMap extends LinkedHashMap<Integer, Integer> {
public static void main(String[] args) {
A63_LRUCacheWithLinkedHashMap lRUCache = new A63_LRUCacheWithLinkedHashMap(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
System.out.println(lRUCache.get(1)); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
System.out.println(lRUCache.get(2));; // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
}
private int capacity;
// 构造方式
public A63_LRUCacheWithLinkedHashMap(int capacity) {
// accessOrder: 如果key被访问,是否把当前key放置在最前位置
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
public Integer get(Object key) {
if (super.get(key) == null) {
return -1;
}
return super.get(key);
}
@Override
public Integer put(Integer key, Integer value) {
return super.put(key, value);
}
// hashmap默认情况下当数据变多的时候,会自动扩充容量
// put或者putAll的时候会调用该方法
// 如果是false,是不会删除的数据的
// 如果是true,则会删除最老的数据
// 我们这里当容量大于指定容量的时候,需要删除掉最老的数据
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
// size()是父类的一个方法,计算当前大小
// 当前数据大于指定容量的时候,true:删除掉最老的数据
return size() > capacity;
}
}
思路2: 通过自定义方法实现:hashmap + 双链表
package com.shangguigu.dachang.algrithm.A06_hashTable;
import java.util.HashMap;
/**
* @author : 不二
* @date : 2022/4/15-下午1:56
* @desc : 146. LRU 缓存
* https://leetcode-cn.com/problems/lru-cache/
**/
public class A65_LRUCache_Custom {
public static void main(String[] args) {
A65_LRUCache_Custom lRUCache = new A65_LRUCache_Custom(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
System.out.println(lRUCache.get(1)); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
System.out.println(lRUCache.get(2));; // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
System.out.println(lRUCache.get(1));; // 返回 -1 (未找到)
System.out.println(lRUCache.get(3));; // 返回 3
System.out.println(lRUCache.get(4));; // 返回 4
}
// 定义双向链表节点, 用于存储数据之间的关系,方便后续删除
// 这些节点都是存储在hashmap中方便删除更新
// 然后节点是双向当,方便进行更改位置和找到老数据等等
class Node {
int key;
int value;
Node next;
Node prev;
// 方便进行构造
public Node(int key, int value) {
this.key = key;
this.value = value;
}
// 方便哨兵节点创建
public Node() {
}
}
private Node head, tail;
// 定义hash表,用于存储对应的数据的媒介
private HashMap<Integer, Node> hashMap = new HashMap<>();
// 容量大小,当数据个数大于指定个数时候, 删除最老数据
private int capacity;
// 当前数据的个数
private int size;
// 构造LRUCache
public A65_LRUCache_Custom(int capacity) {
this.capacity = capacity;
this.size = 0;
// 把头尾的哨兵节点定义好
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
}
// get方法
public Integer get(int key) {
Node node = hashMap.get(key);
if (node == null) {
return -1;
}
// 如果存在,需要把当前node移动到末尾
moveNodeToTail(node);
return node.value;
}
public void put(int key, int value) {
Node node = hashMap.get(key);
if (node != null) {
// 如果存在,需要更新数据
node.value = value;
// 并把这个节点放到链表到最后
moveNodeToTail(node);
} else {
// 说明该节点不存在
Node newNode = new Node(key, value);
hashMap.put(key, newNode);
addNodeToTail(newNode);
// 直接封装成方法
/*// 并把node放到链表最后
tail.prev.next = newNode;
newNode.next = tail;
// 这里到时候, tail的前一个节点还是原先的那个,这里通过newNode.pre 指向 原先那个, 把原先最后一个变成倒数第二个
newNode.prev = tail.prev;
tail.prev = newNode;*/
// 当前数据++
size++;
// 如果超出了容量限制,需要删除链表头数据
if (size > capacity) {
Node firstNode = removeFirstNode();
hashMap.remove(firstNode.key);
size--;
}
}
}
private void moveNodeToTail(Node newNode) {
// Node theNode = newNode;
// 把需要移动的节点的前一个节点指向后一个节点
// 其实就是删除一个节点
removeNode(newNode);
// 然后把newNode添加在末尾即可
addNodeToTail(newNode);
}
private void addNodeToTail(Node newNode) {
/*// 并把node放到链表最后
tail.prev.next = newNode;
newNode.next = tail;
// 这里到时候, tail的前一个节点还是原先的那个,这里通过newNode.pre 指向 原先那个, 把原先最后一个变成倒数第二个
newNode.prev = tail.prev;
tail.prev = newNode;*/
// 换一种更简单的思路
newNode.prev = tail.prev;
newNode.next = tail;
newNode.prev.next = newNode;
newNode.next.prev = newNode;
}
private Node removeFirstNode() {
Node firstNode = head.next;
// 这里直接更改也行
/*head.next = head.next.next;
head.next.prev = head;*/
// 这里直接拿到第一个节点,删除
removeNode(firstNode);
return firstNode;
}
// 删除某个节点
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}