文章目录
1.背景
我们在各类缓存等应用场景中都涉及到LRU算法。接下来我仔细分析一下LRU算法的设计思路及代码实现。以及介绍一些扩展知识点。
2.LRU详解
2.1 什么是LRU?
LRU是什么?按照英文的直接原义就是Least Recently Used,最近最久未使用法(去掉最久未使用的数据),它是按照一个非常著名的计算机操作系统基础理论得来的:最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间(顺序),附加指标是使用的次数。在计算机中大量使用了这个机制,它的合理性在于优先筛选热点数据,所谓热点数据,就是最近最多使用的数据!因此,利用LRU我们可以解决很多实际开发中的问题,并且很符合业务场景。
2.2 LRU算法设计
2.2.1、数据结构。
由LRU概念原理分析,我们需要的这个数据结构必须满足条件:查找快,插入快,删除快,有顺序之分。那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
双向链表有一个特点就是它的链表是双路的,我们定义好头节点和尾节点,然后利用先进先出(FIFO),最近被放入的数据会最早被获取。其中主要涉及到新增、访问、修改、删除操作。
2.2.2、新增&修改
新增和修改的逻辑是一样的:
- 先查询链表中此key是否存在。
- 如果存在,则为修改,用新值覆盖旧值。将整个节点移动至队尾。
- 如果不存在,则是新增。先判断是否超过链表容量,如果超过则将队首(最久未使用)节点删除后进行下一步。如果未超过容量,直接进行下一步。
- 将新节点,放在链表队尾,其他的元素顺序往队首移动;
具体实现时,判断链表容量时,还有扩容一说。具体原理参考HashMap存储扩容原理。
2.2.3、访问
- 先查询链表中此key是否存在。若不存在则返回null。
- 若存在,则返回对应的value。
- 若节点在队尾则不用管。若在队中或队首,则将对应节点移动至队尾。
- 返回为null还有一种情况,即对应key的节点存在,但其值为null。
2.2.4、删除
- 判断节点存在。
- 移除节点,其他节点顺序移动位置。
3.LRU实现
3.1 Java自己封装实现
- 定义基本的链表操作节点
public class Node {
//键
Object key;
//值
Object value;
//上一个节点
Node pre;
//下一个节点
Node next;
public Node(Object key, Object value) {
this.key = key;
this.value = value;
}
}
- 链表基本定义
我们定义一个LRU类,然后定义它的大小、容量、队尾节点、队首节点等部分,然后一个基本的构造方法
public class LRU<K, V> {
private int currentSize;//当前的大小
private int capcity;//总容量
private HashMap<K, Node> caches;//所有的node节点
private Node last;//队尾节点
private Node first;//队首节点
public LRU(int size) {
currentSize = 0;
this.capcity = size;
caches = new HashMap<K, Node>(size);
}
- 添加(修改)元素
添加(修改)元素的时候首先判断是不是新的元素,如果是新元素,判断当前的大小是不是大于总容量了,防止超过总链表大小,如果大于的话直接抛弃队首第一个节点,然后再以传入的key\value值创建新的节点。对于已经存在的元素,直接覆盖旧值,再将该元素移动到队尾部,然后保存在map中。
/**
* 添加(修改)元素
* @param key
* @param value
*/
public void put(K key, V value) {
Node node = caches.get(key);
//如果新元素
if (node == null) {
//如果超过元素容纳量
if (caches.size() >= capcity) {
//移除队首第一个节点
caches.remove(first.key);
removeLast();
}
//创建新节点
node = new Node(key,value);
}
//已经存在的元素覆盖旧值
node.value = value;
//把元素移动到队尾部
moveToLast(node);
caches.put(key, node);
}
如下所示,访问key=3这个节点的时候,需要把3移动到队尾,这样能保证整个链表的队尾(最近)节点一定是特点数据(最近使用的数据!)
4. 访问元素
通过key值来访问元素,主要的做法就是先判断如果是不存在的,直接返回null。如果存在,把数据移动到队尾部成为最后的节点,然后再返回旧值。
/**
* 通过key获取元素
* @param key
* @return
*/
public Object get(K key) {
Node node = caches.get(key);
if (node == null) {
return null;
}
//把访问的节点移动到尾部
moveToLast(node);
return node.value;
}
- 节点删除操作
在根据key删除节点的操作中,我们需要做的是把节点的前一个节点的指针指向当前节点下一个位置,再把当前节点的下一个的节点的上一个指向当前节点的前一个,这么说有点绕,我们来画图来看:
/**
* 根据key移除节点
* @param key
* @return
*/
public Object remove(K key) {
Node node = caches.get(key);
if (node != null) {
if (node.pre != null) {
node.pre.next = node.next;
}
if (node.