leetCode学习笔记(java):146: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
思路:题目要求算法时间复杂度为O(1),且数据的存储形式为(key,value)形式,很容易想到要使用到类似于HashMap的数据结构。
再看LRU的定义和工作原理举例:
LRU :是内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
什么是LRU算法? LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。
工作原理举例:
LRU(least recently used)最近最少使用。
假设 序列为 4 3 4 2 3 1 4 2
物理块有3个 则
首轮 4调入内存 4
次轮 3调入内存 3 4
之后 4调入内存 4 3
之后 2调入内存 2 4 3
之后 3调入内存 3 2 4
之后 1调入内存 1 3 2(因为最少使用的是4,所以丢弃4)
之后 4调入内存 4 1 3(原理同上)
最后 2调入内存 2 4 1
从这个例子我们也很容易看出,LRU的工作原理类似于队列,每次将刚刚使用过的数据块(认为这个数据块在之后会经常被使用)放在队尾,队列满的话就将队首的元素进行删除。
这是我最开始的思路,通过Queue和HashMap实现,代码如下:
class LRUCache {
private Queue<Integer> queue;
private Map<Integer,Integer> hashMap;
private int capacity;
public LRUCache(int capacity) {
queue = new LinkedList();
this.capacity = capacity;
hashMap = new HashMap<Integer, Integer>();
}
public int get(int key) {
if(queue.contains(key)){ //如果队列中已经存在key,则在获取value后,将key放到队尾
int value = hashMap.get(key);
queue.remove(key);
queue.offer(key); //将key重新加入到队尾
return value;
}else{
return -1;
}
}
public void put(int key, int value) {
if(queue.size() == capacity && !hashMap.containsKey(key)){
hashMap.remove(queue.poll());
}else if(hashMap.containsKey(key)){ //如果key已经存在,则需要更新value值,
//此时也视为操作该数据所以也要进行key重新入队的操作
hashMap.put(key,value);
queue.remove(key);
queue.offer(key);
}
if(!hashMap.containsKey(key)){ //如果key为新值,则直接将数据加入map,并将key入队
hashMap.put(key,value);
queue.offer(key);
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
虽然结果是通过了,但时间复杂度却高的吓人,接下来进行改进。
原因分析:时间复杂度之所以这么高的原因,应该是因为每次增删都使用了remove()方法,在删除队列中的特定晕元素,必须进行遍历判断,因此当数据量很大时,效率是很低的。
改进方案:参考其他人的代码,大部人都用了LinkedHashMapde 数据结构进行设计,LinkedHashMap是 双链表+HashMap,即可以使插入删除(key,value)的时间复杂的达到O(1),也可以通过双向链表实现节点的快速插入删除,时间复杂度会大大降低。
即用HashMap能保证查找的时间复杂度是O(1),双向链表保证的是增删的时间复杂度是O(1),当然用单向链表也可以,但是不太方便;
也就是用双向链表的插入到头结点和删除结点来代替方法1中的 删除队列key元素remove和**key的重新入队和出队queue.poll(),queue.offer()**的操作来提升增删效率。
双向链表的结构如下:
LinkedHashMap的结构如下:(转自:https://blog.csdn.net/justloveyou_/article/details/71713781)
则以LinkedHashMap为数据结构进行LRU的实现:
先将(key,value)存入dulNode(双向链表的结点),再将(key,dulNode)存入HashMap中,Map进行快速查找(O(1)),链表进行快速增删(O(1)。
public class LRUCache2 {
class dulNode{ //双向链表的结点
int key;
int value;
dulNode pre;
dulNode next;
dulNode(int key,int value){
this.key = key;
this.value = value;
}
}
private dulNode head;
private dulNode tail;
private int size = 0; //记录链表有效数据的长度
private int capacity;
Map<Integer,dulNode> hashMap;
public void removeNode(dulNode node){ //双向链表中删除结点
node.pre.next = node.next;
node.next.pre = node.pre;
}
public void addToHead(dulNode node){ //将结点插入到头结点后,对应队列的入队
node.next = head.next; //初始时head.next 即为tail,相当与 node.next = tail;
head.next.pre = node;
node.pre = head;
head.next = node;
}
public LRUCache2(int capacity) {
hashMap = new HashMap<Integer,dulNode>();
this.capacity = capacity; //初始化最大容量
head = new dulNode(0,0); //初始化头结点
tail = new dulNode(0,0); //初始化尾结点
head.pre = null;
head.next = tail;
tail.next = null;
tail.pre = head;
}
public int get(int key) {
if(hashMap.get(key) != null){
int value = hashMap.get(key).value;
dulNode latest = hashMap.get(key);
removeNode(latest); //每次对结点进行get操作,则视这个结点为最新的,将其删除,
addToHead(latest); //再插入到头结点之后
return value;
}else{
return -1;
}
}
public void put(int key, int value) {
if(hashMap.get(key) !=null ){ //若key已经存在,则需更新key,value,则需先删除原始key结点
removeNode(hashMap.get(key));
hashMap.remove(key);
size--;
}
dulNode newNode = new dulNode(key,value);
hashMap.put(key,newNode);
if(size <capacity){ //链表长度小于阈值则直接加到头结点后面
addToHead(newNode);
size++;
}else{ //如果超过阈值则需先删除尾部前的结点,在将新结点插入到头结点后
dulNode reNode = tail.pre;
removeNode(reNode);
hashMap.remove(reNode.key);
addToHead(newNode);
}
}
}
执行效率成功提高到了106ms,果然对于需要频繁进行增删的数据,链表的效率的确比其他数据结构要高的多。