1.LRU的含义
关于LRU,简单来说就是当内存空间满了。不得不淘汰某些数据时,通常是容量满了,选择最久未被使用的数据进行淘汰。
LRU(Least Recently Used)是一种缓存淘汰策略,它基于这样的假设:在一段时间内,最近被访问的数据更有可能在将来被访问到,而最久未被访问的数据更有可能在将来不再被使用。
LRU算法的核心思想是,当缓存空间已满时,优先淘汰最久未被访问的数据。具体实现方式通常是使用双向链表(Doubly Linked List)和哈希表(Hash Map)的结合。
LRU算法的主要步骤如下:
- 初始化一个容量固定的缓存,用双向链表和哈希表保存数据。
- 当数据被访问时,从哈希表中查找该数据是否存在。
- 如果数据存在,从双向链表中将该节点移动到链表头部,表示最近使用过。
- 如果数据不存在,将数据添加到双向链表的头部,并在哈希表中创建一个对应的键值对。
- 当缓存已满时,要淘汰最久未被使用的数据。
- 在双向链表的尾部找到最久未使用的数据节点,并从双向链表和哈希表中移除该节点。
通过使用LRU算法,可以保证缓存中始终存储最近使用的数据,从而提高缓存的命中率和系统性能。在实际应用中,LRU算法广泛应用于数据库查询缓存、操作系统页面置换等场景。
如按照 7 0 1 2 0 3 0 4 的次序访问
2.hash + 双链表实现LRU
1. Hash的作用用来做到O(1)访问元素,哈希表就是普通的哈希映射,通过缓存数据的键映射到其在双链表的位置。Hash里的数据值key-value结构。value就是我们自己封装的node,key则是键值,也就是Hash的地址。
2. 双向链表用来实现根据访问情况对元素进行排序,双向链表按照被使用的顺序存储这些键值,靠近头的键值是最近使用的,而靠近尾部的键值对是最久未被使用的。
对于LRU的实现,我们重点关注的是get和put操作
对于get操作,首先判断key是否存在
不存在,返回-1;
存在,则key对应的结点是最近被使用过的结点,通过哈希表定位该结点在双向链表的位置。并将其移动到双向链表的头部,最后返回该节点的值,
对于put操作,首先判断key是否存在
如果不存在,使用key和value创建一个新结点,在双向链表的头部添加该节点,并将key和该节点的值添加进哈希表中,然后判断双向链表是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希中的对应项;
如果存在,则和get类似,先通过hash定位,更新value的值,并将该节点移到头部,
在双向链表的实现我们用虚拟头和虚拟尾结点辅助操作,先看几个图示例子,
容量为3,首先缓存1,如图a,之后缓存2和3,结构如图b。
之后4进入,则会删除末尾的1,然后4在头部,如果再次访问2,则2的位置会更新,
LRU算法在很多地方都有使用,基本计算机的各个地方都有涉及,所以要大概清楚其实现原理,代码实现如下:
public class LRUCache {
//定义双链表
class DlinkedNode{
int key;//哈希键值,也就是哈希地址
int value;//我们为您自己封装的元素
DlinkedNode prev;//指向前面元素
DlinkedNode next;//指向后面元素
public DlinkedNode() {
}
public DlinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
//创建HashMap存储元素
private Map<Integer,DlinkedNode> cache = new HashMap<Integer,DlinkedNode>();
//链表存储元素个数
int size;
//链表容量
int capacity;
//虚拟头、虚拟尾
private DlinkedNode head,tail;
//定义链表容量,创建虚拟头尾指针,初始化构造方法
public LRUCache(int capacity){
this.size = 0;
this.capacity = capacity;
//头、尾
head = new DlinkedNode();
tail = new DlinkedNode();
head.next = tail;
tail.prev = head;
}
//关键字存在,返回值,重新刷新缓存,否则返回-1,
public int get(int key){
DlinkedNode node = cache.get(key);
if (node == null){
return -1;
}
//如果key存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
//加入值,存在则通过哈希表定位,修改value,并移到头部,不存在,创建新节点,放到头部,判断容量是否超出,超出删除尾节点
public void put(int key,int value){
DlinkedNode node = cache.get(key);//不存在会返回null
if (node == null){
//key不存在,创建新节点,
DlinkedNode newnode = new DlinkedNode(key,value);
//添加进哈希表
cache.put(key,newnode);
//添加至双链表头部
addToHead(newnode);
++size;
if (size > capacity){
//超出容量,删除双链表尾结点
DlinkedNode tail = removeTail();
//删除哈希表中对应项
cache.remove(tail.key);
--size;
}
}else {
//key存在,先通过哈希定位,再修改value,再移到头部
node.value = value;
moveToHead(node);
}
}
//删除尾部结点
private DlinkedNode removeTail() {
DlinkedNode res = tail.prev;
removeNode(res);
return res;
}
//增加头部结点
private void addToHead(DlinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
//删除结点
public void removeNode(DlinkedNode node){
node.next.prev = node.prev;
node.prev.next = node.next;
}
//删除已存在的,增加新结点
private void moveToHead(DlinkedNode node) {
removeNode(node);
addToHead(node);
}
}