LRU算法
什么是LRU算法
LRU算法又叫删除最近最未使用算法,是一种缓存淘汰策略。
计算机中的容量是有限的,如果内存满了的话,那么就要删除旧的数据来满足让新数据可以填充进入,那么问题来了,什么样的数据就是要被删除的数据?
LRU缓存算法是一种常用的策略。全名又称Least Recently Used,也就是说我们认为最近使用过的数据应该是是「有用的」,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
使用场景
比如说现在有一个鞋柜,里面可以存放6双鞋,每新买一双鞋,你都要将新鞋存放进入,每次存放的时候,你都会判断一下是否有空余的位子,如果有空位子的话,你就可以成功存放进去,如果没有的话,你就需要忍痛割爱,把最近未穿(时间最长)的那双鞋拿出来,给新鞋腾出位置,让新鞋入库。这个就是典型的LRU设计思路。
希望我拿鞋举例子,可以让你们对LRU算法有一定的了解。相信你们一定可以的。
算法设计思路
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
-
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
-
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
如图所示,在双向链表中头部数据总为活跃度最高的节点,尾部为活跃度最低的节点,若容器已满的话,首先会淘汰尾部节点,将新数据插入头部,以此保证头部总是活跃度最高的节点。
具体实现
- 首先定义数据结构,存放该节点的k值,v值,前继节点和后继节点
/**数据结构*/
class CacheNode {
Object key; // 键
Object value; // 值
CacheNode next; // 后继节点
CacheNode pre; // 前继节点
}
- 定义全局变量:容器(caches),容量大小(capacity),容器中头节点(head)和尾节点(tail)
/**缓存容器*/
HashMap<K, CacheNode> caches;
/**容量大小*/
private int capacity;
/**头结点*/
private CacheNode head;
/**尾节点*/
private CacheNode tail;
- 初始化容器大小,通过构造方法进行初始化
/**实例化*/
public LRUCache(int size) {
// 容器大小
this.capacity = size;
// 实例化容器
caches = new HashMap<K, CacheNode>(size);
}
- 编写put方法
/**
* 添加k-v
* @param k 键
* @param v 值
* @return 值
*/
public V put(K k, V v) {
// 1. 从容器中查找是否存在
CacheNode cacheNode = caches.get(k);
// 2. 若存在于容器中 将CacheNode节点移到容器队首
// 3. 若不存在与容器中
if (cacheNode == null) {
// 3.1 容器实际大小 是否大于 所允许存放的最大数量
if (caches.size() >= capacity) {
// 3.1.1 若容器实际大小大于所允许存放的最大数量 将容器尾部的CacheNode节点(活跃度不高的节点)删除
caches.remove(tail.key);
// 将tail节点指向它前一个节点 更新tail节点的指向
removeLast();
}
// 3.1.2 若容器实际大小小于所允许存放的最大数量
// 3.1.2.1 将其封装成CacheNode节点 投入到容器中
cacheNode = new CacheNode();
cacheNode.key = k;
}
cacheNode.value = v;
// 将节点 移到 容器头部 保证 容器中的节点是按活跃度排序的
moveToFirst(cacheNode);
// 将当前节点封装成CacheNode 填充到容器中
caches.put(k ,cacheNode);
return v;
}
- 编写get方法
/**
* 获取该节点
* @param k
* @return
*/
public V get(K k) {
// 查询该k 是否存在于容器中
CacheNode node = caches.get(k);
if (node == null) {
return null;
}
// 将该节点移到容器头部
moveToFirst(node);
return (V)node.value;
}
- 自定义的方法
moveToFirst()方法
/**
* 将CacheNode移到容器头部 & 更新head和tail节点的指向
* 0. 若当前节点等于head节点 无需移到 直接return
* 1. 若当前节点的next节点不为空 将当前节点的后继节点的前继节点指向当前节点的前继节点
* 2. 若当前节点的pre节点不为空 将当前节点的前继节点的后继节点指向当前节点的后继节点
* 3. 将tail节点等于当前节点 更新tail节点指向 将tail节点指向当前节点的前继节点
* 4. 若head和tail节点都为空 直接将当前节点赋值给head和tail节点 -- 表示第一次添加k-v键值对
* @param cacheNode
*/
private void moveToFirst(CacheNode cacheNode) {
// 若当前节点等于head节点 无需移到 直接return
if (head == cacheNode) return;
// 若当前节点的next节点不为空 将当前节点的后继节点的前继节点指向当前节点的前继节点
if (cacheNode.next != null) cacheNode.next.pre = cacheNode.pre;
// 若当前节点的pre节点不为空 将当前节点的前继节点的后继节点指向当前节点的后继节点
if (cacheNode.pre != null) cacheNode.pre.next = cacheNode.next;
// 将tail节点等于当前节点 更新tail节点指向 将tail节点指向当前节点的前继节点
if (tail == cacheNode) tail = cacheNode.pre;
// 若head和tail节点都为空 直接将当前节点赋值给head和tail节点 -- 表示第一次添加k-v键值对
if (head == null || tail == null) {
head = tail = cacheNode;
return;
}
// 将当前节点的next指向head节点
cacheNode.next = head;
// head节点的前继节点指向当前节点
head.pre = cacheNode;
// 将当前节点赋值给head节点
head = cacheNode;
// 将当前节点的前继节点置为空
head.pre = null;
}
removeLast()方法
/**
* 将tail节点指向tail.pre
*/
private void removeLast() {
if (tail != null) {
// 将tail节点更新为它的前继节点
tail = tail.pre;
// 若tail节点为空 表示容器中无节点 将head置为空
if (tail == null) head = null;
// 否则 将tail节点的next置为空
else tail.next = null;
}
}
toString()方法
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
CacheNode node = head;
while (node != null) {
sb.append(String.format("%s:%s ", node.key, node.value));
node = node.next;
}
return sb.toString();
}
- 代码测试
编写测试方法
public static void main(String[] args) {
LRUCache<Integer, String> lru = new LRUCache<>(3);
lru.put(1, "a");
System.out.println(lru.toString());
lru.put(2, "b"); // 2:b 1:a
System.out.println(lru.toString());
lru.put(3, "c"); // 3:c 2:b 1:a
System.out.println(lru.toString());
lru.put(4, "d"); // 4:d 3:c 2:b
System.out.println(lru.toString());
lru.put(1, "aa"); // 1:aa 4:d 3:c
System.out.println(lru.toString());
lru.put(2, "bb"); // 2:bb 1:aa 4:d
System.out.println(lru.toString());
lru.put(5, "e"); // 5:e 2:bb 1:aa
System.out.println(lru.toString());
lru.get(1); // 1:aa 5:e 2:bb
System.out.println(lru.toString());
}
- 结果如图所示
到此,LRU算法已大功告成,恭喜你们,又掌握一门算法,离大厂又进了一步。奥利给 !!!
公众号
领取Java面试资料的,关注公众号回复关键字【888】,即可领取大厂面试攻略
代码已收录到github中,如有需要,可自行下载:
https://github.com/memo012/java_memo