1. 什么是LRU cache?
LRU+cache
LRU(Least Recently Used)是一种淘汰策略,最近最久未使用被淘汰
与之对应的有FIFO:先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(离现在最久的,也就是旧的数据)被淘汰
cache:缓存,容量小,速度快
LRU cache,算法思想是:将最近访问的数据挪动到头部,如果下次还是访问这个数据,那么就能在靠前的位置访问到(缓存大小固定,需要淘汰最近最少访问数据)
这点其实是运用了时间局部性原理:最近一次访问的位置,下一次也很可能访问
2. 实现思路:
- 使用双向链表
- 使用哨兵节点,pre 指向尾节点,next 指向头结点
- 插入操作:push(1,先查找,如果有,挪动到头部2,如果没有,检查cache 是否到达大容量,如果达到了,删除尾节点,将新节点插入到头结点)
- 查找操作:如果没找到,返回null,如果找到,先将目标节点调整到链表头,然后返回该节点
3. 数据举例说明:
假设输入序列是 1,3,2,3,4
输入完1,链表效果是
1->哨兵->1
输入完3,效果是:(使用头插法,新的元素总是在头结点)
1 ->哨兵->3->1
输入2,链表效果:
1 ->哨兵->2->3->1
输入3:(注意cache 中已经存在3,只需要将该节点调整到头结点即可)
1 ->哨兵->3->2->1
输入4:注意,cache 大小已经达到最大缓存限制,而且 cache 不存在4,那么需要线删除尾节点(也就是1,然后将4插入到头部)
2->哨兵->4->3->2
4. java 代码实现:
import java.util.Scanner;
/**
* @author wangwei
* @date 2019/3/28 21:30
* @classDescription 最近最少使用, 放在链表末尾
* 使用双向链表:
* 1,pop (查找元素,将目标元素放到链表头部,然后返回数据)
* 2,push 添加元素,检查size,size如果超过最大,移除最后一个节点,将新节点插入链表头部
*/
public class LRUCache<T> {
private int MAX_SIZE;// cache 最大容量
private int size;
private DoNode sentryNode;// 虚拟节点,pre 指向头节点,next 指向尾节点
public LRUCache(int MAX_SIZE) {
this.MAX_SIZE = MAX_SIZE;
sentryNode = new DoNode(Integer.MIN_VALUE);
sentryNode.next = sentryNode;
sentryNode.pre = sentryNode;
}
// 查找数据,并将节点返回
public DoNode<T> pop(T data) {
DoNode target = search(data);
if (null == target) {
return null;
}
//将target 调整到头部
moveExistsNodeToHead(target);
return target;
}
public void push(T data) {
DoNode lookResult = search(data);
if (lookResult != null) {
moveExistsNodeToHead(lookResult);
return;
}
if (size >= MAX_SIZE) {
removeLast();
}
// 插入新节点
insertBeforeHead(data);
}
private DoNode<T> search(T data) {
DoNode result = sentryNode.next;
// 不要绕圈查找,如果到了哨兵还没查找到,说明缓存不存在
while (result != null && (!result.equals(sentryNode))) {
if (result.data.equals(data)) {
break;
}
result=result.next;
}
return sentryNode.equals(result) ? null : result;
}
//将node 调整缓存中存在的节点到头部
private void moveExistsNodeToHead(DoNode target) {
//将target 调整到头部
target.pre.next = target.next;
target.next=target.pre;
target.pre = sentryNode;
sentryNode.next.pre=target;
target.next=sentryNode.next;
sentryNode.next = target;
}
//删除尾节点
private void removeLast() {
if (size <= 0) {
throw new RuntimeException(" 不能继续删除尾节点");
}
sentryNode.pre = sentryNode.pre.pre;
sentryNode.pre.next = sentryNode;
size--;
}
// 头插入节点
private void insertBeforeHead(T data) {
DoNode node = new DoNode(data);
node.pre = sentryNode;
node.next = sentryNode.next;
node.next.pre=node;
sentryNode.next = node;
size++;
}
public static void main(String[] args) {
LRUCache<Integer> cache = new LRUCache<>(3);
Scanner in = new Scanner(System.in);
int [] input={1,2,3,2,3};
for(int data:input){
cache.push(data);
// System.out.println("当前cache:" + cache.toString());
}
}
class DoNode<T> {
T data;
DoNode pre;
DoNode next;
public DoNode(T data) {
this.data = data;
}
}
}
5. 分析:
- 为什么使用双向链表?
方便删除节点,以及插入节点 - 为什么使用哨兵
方便快速定位头结点与尾节点(插入是在头结点,删除是在尾节点)
6另一种更通用的版本实现:
将节点定义由单独的data改为 K+DATA的
package top.forethought.linklist;
import java.util.Scanner;
/**
* @author wangwei
* @date 2019/3/28 21:30
* @classDescription 最近最少使用, 放在链表末尾
* 使用双向链表:
* 1,pop (查找元素,将目标元素放到链表头部,然后返回数据)
* 2,push 添加元素,检查size,size如果超过最大,移除最后一个节点,将新节点插入链表头部
*/
public class LRUCache<K,V> {
private int MAX_SIZE;// cache 最大容量
private int size;
private DoNode sentryNode;// 虚拟节点,pre 指向头节点,next 指向尾节点
public LRUCache(int MAX_SIZE) {
this.MAX_SIZE = MAX_SIZE;
sentryNode = new DoNode();
sentryNode.next = sentryNode;
sentryNode.pre = sentryNode;
}
// 查找数据,并将节点返回
public DoNode<K,V> pop(K key) {
DoNode target = search(key);
if (null == target) {
return null;
}
//将target 调整到头部
moveExistsNodeToHead(target);
return target;
}
public void push(K key,V data) {
DoNode lookResult = search(key);
if (lookResult != null) {
moveExistsNodeToHead(lookResult);
return;
}
if (size >= MAX_SIZE) {
removeLast();
}
// 插入新节点
insertBeforeHead(key,data);
}
private DoNode search(K key) {
DoNode result = sentryNode.next;
// 不要绕圈查找,如果到了哨兵还没查找到,说明缓存不存在
while (result != null && (!result.equals(sentryNode))) {
if (key.equals(result.key)) {
break;
}
result = result.next;
}
return sentryNode.equals(result) ? null : result;
}
//将node 调整缓存中存在的节点到头部
private void moveExistsNodeToHead(DoNode target) {
//将target 调整到头部
target.pre.next = target.next;
target.next = target.pre;
target.pre = sentryNode;
sentryNode.next.pre = target;
target.next = sentryNode.next;
sentryNode.next = target;
}
//删除尾节点
private void removeLast() {
if (size <= 0) {
throw new RuntimeException(" 不能继续删除尾节点");
}
sentryNode.pre = sentryNode.pre.pre;
sentryNode.pre.next = sentryNode;
size--;
}
// 头插入节点
private void insertBeforeHead(K key,V data) {
DoNode node = new DoNode(key,data);
node.pre = sentryNode;
node.next = sentryNode.next;
node.next.pre = node;
sentryNode.next = node;
size++;
}
public static void main(String[] args) {
LRUCache<Integer,Integer> cache = new LRUCache<>(3);
Scanner in = new Scanner(System.in);
Integer[] input = {1, 2, 3, 2, 3};
for (Integer data : input) {
Integer key=data-1;
cache.push(key,data);
}
}
class DoNode<K,V> {
K key;
V data;
DoNode pre;
DoNode next;
public DoNode(K key,V data) {
this.key=key;
this.data = data;
}
public DoNode() {
}
}
}
其他改进思路
使用hashMap 存储key 到节点的映射,提高查询速度