一、LRU算法简介
LRU算法的原理讲解,会的可以直接看代码
LRU,即最近最少使用算法。算法的逻辑是,当数量满了后,再添加新的值后,需要先淘汰一个旧值。
而这个旧指就是最近最少使用的值。
那么问题就是,如何确认最近最少使用的值?
有人会说,我记录每个值的使用次数和最近使用时间就可以了。
逻辑上没错,但一方面记录数据太多,另一方面每次都要遍历整个数据结构,复杂度O(n)。
那么有没有更好的方法能够在O(1)时间内得到要淘汰的旧值呢?
答案当然是有,那就是用队列。将每次使用到的值放到队列队首,那么淘汰的值就是队列尾部的值即可。这样就是O(1)了。
但仅仅将值放到队首是不够的,因为值可能已经存在了,那么我们还需要将队列中的该值给删除掉,
O(1)从队列上删除某个已知的节点(暂不考虑得到这个节点),其实带前置节点的单向队列和双向队列均可实现。
但后面的分析表明双向链表更合适。
目前我们已经分析完添加新节点时的数据结构。但是LRU算法的核心是读
读的不仅仅要把数据读出,还要把读的数据从队列中取出放到队首。
首先如何解决O(1)读呢?空间换时间,Map就行,直接维护Key到队列节点的Map。
这样读数据,只需要从Map中就可以获得数据了。Map的get的平均复杂度为O(1)
而通过Map可以直接获得队列节点,从而直接从队列删除然后放到队首的操作,所以双向队列最佳。
从上面的分析可以得出,LRU算法的实现数据结构是双向队列+Map
二、Java代码实现
1、首先定义队列节点,用于实现双向队列。
注意:原本打算直接用LinkedList来作为双向队列Deque,但是看了看源码,1.8的LinkedList删除某个节点的操作是遍历整个队列来进行比对,所以复杂度太高,因此自己实现。不过并不复杂。
class Node{
String key;
String value;
Node pre = null;
Node next = null;
Node(String key, String value){
this.key = key;
this.value = value;
}
}
基础代码就是这样,主要就是pre和next分别指向前一个和后一个的指针。key就是作为Map的key,value就是其他要存储的值,有没有随意。
2、定义LRU的基本结构
public class LRUTest {
Map<String, Node> map = new HashMap<>();
int size = 0;
int maxSize;
Node head;
Node tail;
LRUTest(int cap){
this.maxSize = cap;
head = new Node("head", "head");
tail = new Node("tail", "tail");
head.next = tail;
tail.pre = head;
}
}
其中Map就是存储已经包含的节点,size是当前存储的元素数量。maxSize就是最大存储数量。head和tail分别是头尾节点。 3、公共方法定义:主要定义将节点从队列中取出和将节点放入队列首。
//从双向链表上删除节点
private void deleteNode(Node node){
node.next.pre = node.pre;
node.pre.next = node.next;
}
//把节点放到队首
private void addHead(Node node){
node.next = head.next;
head.next = node;
node.pre = head;
node.next.pre = node;
}
4、定义add方法,详情见注释
//添加add
public void add(String key, String value){
Node newNode = new Node(key, value);
//判断是否存在
if(map.containsKey(key)){
//存在就直接取出,然后放到队首
Node node = map.get(key);
//取出
deleteNode(node);
//放在队首
addHead(node);
return;
}
//满了就去掉一个
if(size == maxSize){
//从map中删除
map.remove(tail.pre.key);
//删除最后一个
deleteNode(tail.pre);
//数量-1
size--;
}
//添加到队首
addHead(newNode);
//添加到Map中
map.put(key, newNode);
//数量+1
size++;
}
5、定义获得数据的方法,有就返回节点,没有就是null
//读取
public Node get(String key){
if(!map.containsKey(key)){
return null;
}
Node node = map.get(key);
deleteNode(node);
addHead(node);
return node;
}
最终代码
最终的代码如下,测试也已经通过了
public class LRUTest {
Map<String, Node> map = new HashMap<>();
int size = 0;
int maxSize;
Node head;
Node tail;
//节点定义
class Node{
String key;
String value;
Node pre = null;
Node next = null;
Node(String key, String value){
this.key = key;
this.value = value;
}
@Override
public String toString() {
return key + " " + value;
}
}
LRUTest(int cap){
this.maxSize = cap;
head = new Node("head", "head");
tail = new Node("tail", "tail");
head.next = tail;
tail.pre = head;
}
//添加add
public void add(String key, String value){
Node newNode = new Node(key, value);
//判断是否存在
if(map.containsKey(key)){
//存在就直接取出,然后放到队首
Node node = map.get(key);
//取出
deleteNode(node);
//放在队首
addHead(node);
return;
}
//满了就去掉一个
if(size == maxSize){
//从map中删除
map.remove(tail.pre.key);
//删除最后一个
deleteNode(tail.pre);
//数量-1
size--;
}
//添加到队首
addHead(newNode);
//添加到Map中
map.put(key, newNode);
//数量+1
size++;
}
//读取
public Node get(String key){
if(!map.containsKey(key)){
return null;
}
Node node = map.get(key);
deleteNode(node);
addHead(node);
return node;
}
//遍历
public void printAll(){
System.out.println("-------");
Node node = head;
while(node.next != tail){
System.out.println(node.next.toString());
node = node.next;
}
System.out.println("*****");
}
//从双向链表上删除节点
private void deleteNode(Node node){
node.next.pre = node.pre;
node.pre.next = node.next;
}
//把节点放到队首
private void addHead(Node node){
node.next = head.next;
head.next = node;
node.pre = head;
node.next.pre = node;
}
}
小结
LRU算法的低时间复杂度实现,主要是依靠双向链表和Map来实现的。双向链表实现快速的淘汰元素或者将元素从队列中移到队首。而Map则提供快速的定位查找节点在队列的位置,提升查找速度。(就是空间换时间呗)