手把手教你LRU算法思想,从底层开始写LRU实现

在手撕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算法底层其实是双向链表,我们刚刚已经建立起了一个结点,那么我们要把结点进一步封装成为双向链表,这个链表要提供一下操作

  1. 从链表的头部加入一个结点(我们要有一个虚拟节点定位到头节点)
  2. 从链表的尾部删除一个结点(我们要有一个虚拟节点定位到尾部节点)
  3. 我们还要做到删除任意的结点

经过以上的分析,我们就可以知道,双向链表里面的属性:虚拟头节点,虚拟尾部结点,由于我们的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机制是什么?

添加操作

  1. 如果当前容量够了,直接进去
  2. 如果当前容量不够,则删除最近最久未使用的,然后将新的进队
  3. 我们还要注意一个点,就是当我们设置的的值,在当前的容量已经存在的时候,我们做的是更新的操作

删除操作

  1. 当我们是删除任意一个结点的时候,传入一个结点,然后改变指向
  2. 如果我们删除的是最后一个结点的时候,我们就调用第一个方法,传入的结点就是我们通过tail定位到的最后的一个结点

获取操作

  1. 我们获取到一个缓存的时候,说明我们已经使用过了,要把他的位置进行更新
  2. 我们通过一个更加直观的操作去想这个问题,我们再缓存中删除这个值,再从队列头加入这个值,这样就减去了很多复杂的指针指向的操作
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);

    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值