在手撕LRU算法之前,我们要先知道LRU算法是什么,思想是什么?
所谓的LRU算法,就是Least Recently Used,就是最近最少使用算法。算法的思想就是,当我设置的LRU缓存容量为3的时候,在新加入一个容量之前,如果还有剩余容量,则直接加入,如果已经满了,则去掉最近最少使用的,也就是最远的那个。
用队列来说,就是每次我新加入一个对象都是从队头插入的,如果容量充足,则直接加入;如果容量不够了,我就要把一个元素从队尾出队之后,我在将新的元素加入进去。这样就是一个LRU的基本思想。值得注意的是,如果我设置LRU缓存容量为3,加入队列的顺序为1,2,3;当我拿起2缓存的时候,队列的情况就变成了1,3,2 。也就是说,当我使用LRU缓存的时候,变相相当于我们将2这个缓存去掉了,然后又从队列头加进去了。这样的一系列操作就是LRU缓存设置的核心思想。
我们现在从最底层的结点去实现,我们刚刚说了,可以使用队列,我们也可以自己使用一个链表去实现,因为我们要获取尾结点和头节点,在删除操作的时候,要先找到当前结点(Node node = tail.pre),找到当前结点的前一个结点(Node pre = node.pre),所以,我们底层的链表其实就是双向链表,那么对于节点来说,自然就是有两个指针,一个向前,一个向后,我们还有最基本的key-value。那么最底层的Node结点我们就可以这样去设置
class Node {
int key, value;
Node pre, next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "["+ key +":" + value + "]";
}
}
我们刚刚说了,LRU算法底层其实是双向链表,我们刚刚已经建立起了一个结点,那么我们要把结点进一步封装成为双向链表,这个链表要提供一下操作
- 从链表的头部加入一个结点(我们要有一个虚拟节点定位到头节点)
- 从链表的尾部删除一个结点(我们要有一个虚拟节点定位到尾部节点)
- 我们还要做到删除任意的结点
经过以上的分析,我们就可以知道,双向链表里面的属性:虚拟头节点,虚拟尾部结点,由于我们的LRU是有容量的,所以还可以加上一个size记录我们当前链表已经存储的容量。当然了,不加也可以,我们还要在外部在进行进一步的封装。
对于链表的添加删除操作还不熟悉的同学,应该好好补补了!
class DoubleList {
private Node head, tail;
private int size;
public DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.pre = head;
}
/**
* 在头节点删除
* @param node
*/
public void addFirst(Node node){
node.next = head.next;
node.pre = head;
head.next.pre = node;
head.next = node;
size++;
}
/**
* 删除尾部结点
*/
public Node removeLast(){
if(size<1){
return null;
}
//这个是要被删除的结点
Node node = tail.pre;
return remove(node);
}
/**
* 删除某一个结点
* @param node
* @return
*/
public Node remove(Node node){
//重新建立连接
node.pre.next = node.next;
node.next.pre = node.pre;
//断开连接
node.pre = null;
node.next = null;
size--;
return node;
}
public int getSize() {
return size;
}
public void showData(){
Node node = head.next;
while (node!=tail){
System.out.print(node.toString()+", ");
node = node.next;
}
System.out.println("==== size:"+size+" ====");
}
}
我们做完了双向链表的封装,我们刚刚讲到了,我们要做到可以任意删除当前缓存的某个值,那么我们要怎么做到当前缓存中的值呢?一个最简单的方法就是要保留堆某个结点的应用,这样我们就可以做到找到当前结点的前后结点,并通过修改指针的指向做到删除结点的操作。那么保留某一个结点的应用最简单的就是存起来了,我们可以通过HashMap来保存结点的引用,这样我们就能够最快的做到定位到某一个结点。
我们现在进一步做封装吧!
我们想一下我们最开始的讲的LRU机制是什么?
添加操作
- 如果当前容量够了,直接进去
- 如果当前容量不够,则删除最近最久未使用的,然后将新的进队
- 我们还要注意一个点,就是当我们设置的的值,在当前的容量已经存在的时候,我们做的是更新的操作
删除操作
- 当我们是删除任意一个结点的时候,传入一个结点,然后改变指向
- 如果我们删除的是最后一个结点的时候,我们就调用第一个方法,传入的结点就是我们通过tail定位到的最后的一个结点
获取操作
- 我们获取到一个缓存的时候,说明我们已经使用过了,要把他的位置进行更新
- 我们通过一个更加直观的操作去想这个问题,我们再缓存中删除这个值,再从队列头加入这个值,这样就减去了很多复杂的指针指向的操作
package com.zhongruan.LRUTest;
import java.util.HashMap;
import java.util.Map;
/**
* @author weirdo
* @date 2020-11-03 21:48
* @email 1435380700@qq.com
*/
public class LRU {
private Map<Integer,Node> map;
private DoubleList doubleList;
private int capacity;
public LRU(){
this(8);
}
/**
初始化,如果当前没有指定初始化容量的话,则默认为8
*/
public LRU(int capacity) {
this.map = new HashMap<>();
this.doubleList = new DoubleList();
this.capacity = capacity;
}
/**
进行赋值操作
我们首先要知道,当前的缓存里面有没有这个值,如果存在的话,map更新值,队列位置修改
如果不存在的话,则进行容量判断,容量充足的话,直接进队,map保存引用
如果容量不足的话,删除最近最久未使用的,加入新的结点,map保存引用
*/
public void put(int key,int val){
//先去找一下有没有这个值
if(map.containsKey(key)){
Node node = map.get(key);
node.value = val;
doubleList.remove(node);
doubleList.addFirst(node);
}else{
//判断当前的数量
if(capacity==doubleList.getSize()){
//说明已经满了,删除尾部的数据
Node node = doubleList.removeLast();
map.remove(node.key);
}
Node newNode = new Node(key,val);
doubleList.addFirst(newNode);
map.put(key,newNode);
}
doubleList.showData();
}
/**
获取操作
队列位置修改即可
*/
public int get(int key){
if(!map.containsKey(key)){
return -1;
}
//把这个键删除并提到队头
Node node = map.get(key);
doubleList.remove(node);
doubleList.addFirst(node);
doubleList.showData();
return node.value;
}
/*
删除操作
删除队列元素,删除map引用
*/
public void remove(int key){
if(map.containsKey(key)){
Node node = map.get(key);
Node remove = doubleList.remove(node);
map.remove(remove.key);
}
doubleList.showData();
}
}
做完了以上的操作之后,我们的LRU缓存机制就算是做好了。我们来测试一下。为了方便,我写了一个showData方法来查看当前缓存存在的情况
package com.zhongruan.LRUTest;
/**
* @author weirdo
* @date 2020-11-03 23:41
* @email 1435380700@qq.com
*/
public class Main {
public static void main(String[] args) {
LRU cache = new LRU(3);
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
cache.put(4,4);
cache.get(3);
cache.remove(1);
cache.put(1,1);
cache.put(3,6);
}
}