LeetCode104:LRU缓存机制_java实现(史上最通俗易懂)
题目概述:
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:
-
获取数据 get 和 写入数据 put 。
-
获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
-
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:
你是否可以在 O(1) 时间复杂度内完成这两种操作?
思路分析:
什么是LRU:
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
分析思路:
-
分析题目联想到LinkedHashMap,hashmap+一种特殊的数据结构,能够按照存入顺序且最近经常使用(LRU)原则来读取数据。
-
我们需要一个hashmap实现我们key,value关系。我们怎么形成排序的hashmap?
-
我们想到可以用链表实现顺序,如果想按照LRU,我们可以按照头插法,让最后一个节点是我们最久未访问的。
-
秒!
-
仔细思考,我们最近最少使用,虽然没有用计数器判断哪一个对象使用了多少次,每一次头插法让它获得了最不被淘汰的那一个。
-
可是如果我们链表满了该如何清除数据?当然根据LRU,我们每次头插法,那么尾节点是我们最不常用的。删除尾节点。
-
每一次访问,我们需要把权重增加,就是通过将数据插到第一个位置,保证我们访问的数据尊重LRU。
综上所述:
-
加入元素,不仅加入hashmap中,还要加入到我们链表中,并且才用头插法,如果链表满了,我们需要清除最后一个。
-
获取元素,不仅仅是返回元素,我们还需要把他移动到第一个元素位置,代表我们尽量不淘汰它,因为它刚才使用了。
-
淘汰元素,我们淘汰最后一个元素,就是最早加入队伍,却最近没有调用的。
代码实现:
public class LRUCache {
// 实现一个lru map 类似于linkedMap
// 手写一个双向链表
class MyNode {
// 实现key value 关系
int key, value;
// 双向链表有两个构造方法
MyNode() { }
MyNode(int key, int value) {
this.key = key;
this.value = value;
}
// 有前指针和后指针
MyNode preNode, nextNode;
}
// 当前容量,
private int size = 0;
// 容器大小,
private int capacity;
// 缓存map,存真实的,无序的数据
private HashMap<Integer, MyNode> cache;
// 头节点尾节点
private MyNode head, tail;
// 初始化我们的类
LRUCache(int capacity) {
this.capacity = capacity;
head = new MyNode();
tail = new MyNode();
// 刚开始头节点的下一个是尾节点
head.nextNode = tail;
// 尾节点的前一个是头节点
tail.preNode = head;
cache = new HashMap<>();
}
// 实现链表的 加入操作
public void put(int key, int value) {
// 在缓存中获取key
MyNode node = cache.get(key);
// 如果对应的key存在值,则覆盖
if (node != null) {
node.value = value;
// 移动它到队首,删除再移动
movePer(node);
} else {
// 如果对应的key不存在值,则进行缓存
MyNode newNode = new MyNode(key,value);
cache.put(key, newNode);
// 把key放入队首
moveToHead(newNode);
// 我们有序链表数据量加一
++size;
// 如果队溢出了,则释放最后一个
if (size > capacity) {
// 释放尾元素
MyNode tail = removeTail();
// 释放缓存中的元素
cache.remove(tail.key);
// 容量减少
--size;
}
}
}
// 实现链表的 获取操作
public int get(int key) {
// 如果key不存在,
MyNode node = cache.get(key);
if (node == null) {
return -1;
}
// 存在key的话,把它移动到首节点,删除再移动
movePer(node);
//返回key对应的value
return node.value;
}
// 把对应的key放入队首
private void movePer(MyNode node) {
// 先删除
deleteNode(node);
// 再移动
moveToHead(node);
}
private void moveToHead(MyNode node){
// 该元素的前一个元素应该是头节点
node.preNode = head;
// 该元素的后一个元素应该是刚才第一个节点
node.nextNode = head.nextNode;
// 刚才第一个节点的前节点应该是该元素,不能写在下一句代码的后面
head.nextNode.preNode = node;
// 头节点应该指向该节点
head.nextNode = node;
}
// 释放尾元素
private MyNode removeTail() {
// 获取最后一个元素
MyNode lastNode = tail.preNode;
// 删除元素
deleteNode(lastNode);
// 返回最后一个元素
return lastNode;
}
// 删除元素
private void deleteNode(MyNode node) {
// 该元素的前一个元素因该指向该元素的后一个元素
node.preNode.nextNode = node.nextNode;
// 后一个元素的前元素应该是该元素的前元素
node.nextNode.preNode = node.preNode;
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(2);
lruCache.put(1, 1);
lruCache.put(2, 2);
lruCache.put(2, 3);
System.out.println(lruCache.get(2));
}
}