LRU是Least Recently Used的缩写,即最近最少使用。是一种常用的页面置换算法,其所有操作都是在内存中进行。
设计原理:当数据在最近一段时间经常被访问,那么将来也可能被经常访问,这意味着,经常访问的数据需要能快速命中,而不常用的数据,需要淘汰掉。
缓存策略:先进先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。
LRU缓存淘汰算法图解:
基本原理:
第一步:新增数据直接插入到链表头部。
第二步:缓存数据被命中时,把缓存数据移动到链表头部。
第三步:当缓存容量已满时,移除链表尾部的数据,相当于淘汰最近最少使用的缓存。
实施方案:
第一种:数组,但数组是查询速度快,增删慢。
第二种:单链表,但获取结点数据时,需要从头开始遍历查找结点,时间复杂度为O(n)。
第三种:双链表,查询速度慢,增删快,时间复杂度为O(1)。
第四种:HashMap+双链表,HashMap查询时间复杂度是O(1),可采用该方式。
第一步:初始化双链表数据结构,并初始化指定LRU缓存容量等。
/**
* LRU缓存淘汰算法
* 核心思想: 淘汰最近最少使用的数据
* @author ouyangjun
*/
public class LRUCache {
/** 双链表结构 */
static class Node {
Node prev; // 指向上一个结点
Node next; // 指向下一个结点
Object key;
Object value;
public Node(Object key, Object value, Node prev, Node next) {
this.key = key;
this.value = value;
this.prev = prev;
this.next = next;
}
}
private Node first; // 头部结点
private Node last; // 尾部结点
private int size; // 双链表结点数量
private int capacity; // 双链表容量
private HashMap
第二步:增加几个辅助方法。查询LRU缓存数量,清空缓存,查询缓存是否存在。
/** LRU缓存数量 */
public int size() {
return size;
}
/** 直接从Map中查找结点是否存在 */
public Object get(Object key) {
Node node = cacheMap.get(key);
if (node != null) {
return node.value;
}
return null;
}
/** 清理LRU缓存 */
public void clear() {
first = last = null;
cacheMap.clear();
}
/** 打印LRU缓存数据 */
public void printLRUCache() {
System.out.print("LRU缓存数据为: ");
for(Node l = first; l != null; l = l.next) {
System.out.print(l.key + ":" + l.value + " ");
}
System.out.println("\n");
}
第三步:移除尾部结点数据。当LRU缓存容量已满时,需移除。
/** 移除尾部结点 */
public Object removeLast() {
Node l = last;
if (l == null)
return null;
Node p = l.prev;
Object value = l.value;
l.value = null;
l.prev = l; // help GC
last = p;
if (p == null)
first = null;
else
p.next = null;
--size;
return value;
}
第四步:将缓存移动到链表头部。当缓存已存在并且被再次访问时,需移动到链表头部。
/** 将缓存移动到链表头部 */
private void moveToFirst(Node node) {
if (first == node) { // 如果结点已经在头部, 就不需要移动
return;
}
if (node == last) { // 判断结点是否在尾部
last = last.prev;
}
if (node.next != null) { // 判断是否有后续结点
node.next.prev = node.prev;
}
if (node.prev != null) { // 判断是否有前置结点
node.prev.next = node.next;
}
if (first == null || last == null) { // 如果不存在结点, 直接作为头部尾部结点
first = last = node;
return;
}
node.next = first;
first.prev = node;
first = node; // 作为头部结点
first.prev = null; // 头部结点的prev指向null
}
第五步:添加LRU缓存,流程如下。
/** 添加LRU缓存 */
public Object put(Object key, Object value) {
if (key == null || value == null) {
return null;
}
Node node = cacheMap.get(key);
if (node == null) {
if (size() >= capacity) {
cacheMap.remove(key); // 移除Map中的一个对象
removeLast(); // 移除链表尾部结点
}
// 结点不存在时, 新增结点, 缓存数量加1
node = new Node(key, value, null, null);
++size;
}
// 把结点移动到头部
moveToFirst(node);
// 存储到Map中
cacheMap.put(key, node);
return value;
}
第六步:main方法测试
public static void main(String[] args) {
LRUCache lru = new LRUCache(5);
lru.put("A", "1"); // A:1
lru.printLRUCache();
lru.put("B", "2"); // B:2 A:1
lru.printLRUCache();
lru.put("C", "3"); // C:3 B:2 A:1
lru.printLRUCache();
lru.put("D", "4"); // D:4 C:3 B:2 A:1
lru.printLRUCache();
lru.put("E", "5"); // E:5 D:4 C:3 B:2 A:1
lru.printLRUCache();
lru.put("F", "6"); // F:6 E:5 D:4 C:3 B:2
lru.printLRUCache();
lru.put("C", "3"); // C:3 F:6 E:5 D:4 B:2
lru.printLRUCache();
lru.put("G", "7"); // G:7 C:3 F:6 E:5 D:4
lru.printLRUCache();
}
测试打印效果图:
识别二维码关注个人微信公众号
本章完结,待续,欢迎转载!
本文说明:该文章属于原创,如需转载,请标明文章转载来源!