LRU cache简易实现

本文介绍了LRU(Least Recently Used)缓存淘汰策略,解释了其基于时间局部性的思想,并通过一个示例详细阐述了使用双向链表和哨兵节点的实现方式。此外,还讨论了为何选择双向链表和哨兵节点的原因,以及提供了Java代码实现。最后,文章提到了一种更通用的版本实现,即结合HashMap以提升查询效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是LRU cache?

LRU+cache
LRU(Least Recently Used)是一种淘汰策略,最近最久未使用被淘汰
与之对应的有FIFO:先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(离现在最久的,也就是旧的数据)被淘汰
cache:缓存,容量小,速度快
LRU cache,算法思想是:将最近访问的数据挪动到头部,如果下次还是访问这个数据,那么就能在靠前的位置访问到(缓存大小固定,需要淘汰最近最少访问数据)
这点其实是运用了时间局部性原理:最近一次访问的位置,下一次也很可能访问

2. 实现思路:

  1. 使用双向链表
  2. 使用哨兵节点,pre 指向尾节点,next 指向头结点
  3. 插入操作:push(1,先查找,如果有,挪动到头部2,如果没有,检查cache 是否到达大容量,如果达到了,删除尾节点,将新节点插入到头结点)
  4. 查找操作:如果没找到,返回null,如果找到,先将目标节点调整到链表头,然后返回该节点

3. 数据举例说明:

假设输入序列是 1,3,2,3,4
输入完1,链表效果是
1->哨兵->1
输入完3,效果是:(使用头插法,新的元素总是在头结点)
1 ->哨兵->3->1
输入2,链表效果:
1 ->哨兵->2->3->1
输入3:(注意cache 中已经存在3,只需要将该节点调整到头结点即可)
1 ->哨兵->3->2->1
输入4:注意,cache 大小已经达到最大缓存限制,而且 cache 不存在4,那么需要线删除尾节点(也就是1,然后将4插入到头部)
2->哨兵->4->3->2

4. java 代码实现:


import java.util.Scanner;

/**
 * @author wangwei
 * @date 2019/3/28 21:30
 * @classDescription 最近最少使用, 放在链表末尾
 * 使用双向链表:
 * 1,pop (查找元素,将目标元素放到链表头部,然后返回数据)
 * 2,push 添加元素,检查size,size如果超过最大,移除最后一个节点,将新节点插入链表头部
 */
public class LRUCache<T> {
    private int MAX_SIZE;// cache 最大容量
    private int size;
    private DoNode sentryNode;// 虚拟节点,pre 指向头节点,next 指向尾节点

    public LRUCache(int MAX_SIZE) {
        this.MAX_SIZE = MAX_SIZE;
        sentryNode = new DoNode(Integer.MIN_VALUE);
        sentryNode.next = sentryNode;
        sentryNode.pre = sentryNode;

    }

    // 查找数据,并将节点返回
    public DoNode<T> pop(T data) {
        DoNode target = search(data);
        if (null == target) {
            return null;
        }
        //将target 调整到头部
        moveExistsNodeToHead(target);
        return target;
    }

    public void push(T data) {

        DoNode lookResult = search(data);
        if (lookResult != null) {
            moveExistsNodeToHead(lookResult);
            return;
        }


        if (size >= MAX_SIZE) {
            removeLast();
        }
        // 插入新节点
        insertBeforeHead(data);
    }

    private DoNode<T> search(T data) {
        DoNode result = sentryNode.next;
        // 不要绕圈查找,如果到了哨兵还没查找到,说明缓存不存在
        while (result != null && (!result.equals(sentryNode))) {
            if (result.data.equals(data)) {
                break;
            }
            result=result.next;
        }
        return sentryNode.equals(result) ? null : result;
    }

    //将node 调整缓存中存在的节点到头部
    private void moveExistsNodeToHead(DoNode target) {
        //将target 调整到头部
        target.pre.next = target.next;
        target.next=target.pre;

        target.pre = sentryNode;
        sentryNode.next.pre=target;
        target.next=sentryNode.next;
        sentryNode.next = target;
    }

    //删除尾节点
    private void removeLast() {
        if (size <= 0) {
            throw new RuntimeException(" 不能继续删除尾节点");
        }
        sentryNode.pre = sentryNode.pre.pre;
        sentryNode.pre.next = sentryNode;
        size--;
    }

    // 头插入节点
    private void insertBeforeHead(T data) {
        DoNode node = new DoNode(data);
        node.pre = sentryNode;
        node.next = sentryNode.next;
        node.next.pre=node;
        sentryNode.next = node;

        size++;
    }

    public static void main(String[] args) {
        LRUCache<Integer> cache = new LRUCache<>(3);
        Scanner in = new Scanner(System.in);
        int [] input={1,2,3,2,3};
        for(int data:input){
            cache.push(data);
            // System.out.println("当前cache:" + cache.toString());

        }



    }


    class DoNode<T> {
        T data;
        DoNode pre;
        DoNode next;

        public DoNode(T data) {
            this.data = data;
        }


    }
}

5. 分析:

  1. 为什么使用双向链表?
    方便删除节点,以及插入节点
  2. 为什么使用哨兵
    方便快速定位头结点与尾节点(插入是在头结点,删除是在尾节点)

6另一种更通用的版本实现:

将节点定义由单独的data改为 K+DATA的

package top.forethought.linklist;

import java.util.Scanner;

/**
 * @author wangwei
 * @date 2019/3/28 21:30
 * @classDescription 最近最少使用, 放在链表末尾
 * 使用双向链表:
 * 1,pop (查找元素,将目标元素放到链表头部,然后返回数据)
 * 2,push 添加元素,检查size,size如果超过最大,移除最后一个节点,将新节点插入链表头部
 */
public class LRUCache<K,V> {
    private int MAX_SIZE;// cache 最大容量
    private int size;
    private DoNode sentryNode;// 虚拟节点,pre 指向头节点,next 指向尾节点

    public LRUCache(int MAX_SIZE) {
        this.MAX_SIZE = MAX_SIZE;
        sentryNode = new DoNode();
        sentryNode.next = sentryNode;
        sentryNode.pre = sentryNode;

    }

    // 查找数据,并将节点返回
    public DoNode<K,V> pop(K key) {
        DoNode target = search(key);
        if (null == target) {
            return null;
        }
        //将target 调整到头部
        moveExistsNodeToHead(target);
        return target;
    }

    public void push(K key,V data) {

        DoNode lookResult = search(key);
        if (lookResult != null) {
            moveExistsNodeToHead(lookResult);
            return;
        }


        if (size >= MAX_SIZE) {
            removeLast();
        }
        // 插入新节点
        insertBeforeHead(key,data);
    }

    private DoNode search(K key) {
        DoNode result = sentryNode.next;
        // 不要绕圈查找,如果到了哨兵还没查找到,说明缓存不存在
        while (result != null && (!result.equals(sentryNode))) {
            if (key.equals(result.key)) {
                break;
            }
            result = result.next;
        }
        return sentryNode.equals(result) ? null : result;
    }

    //将node 调整缓存中存在的节点到头部
    private void moveExistsNodeToHead(DoNode target) {
        //将target 调整到头部
        target.pre.next = target.next;
        target.next = target.pre;

        target.pre = sentryNode;
        sentryNode.next.pre = target;
        target.next = sentryNode.next;
        sentryNode.next = target;
    }

    //删除尾节点
    private void removeLast() {
        if (size <= 0) {
            throw new RuntimeException(" 不能继续删除尾节点");
        }
        sentryNode.pre = sentryNode.pre.pre;
        sentryNode.pre.next = sentryNode;
        size--;
    }

    // 头插入节点
    private void insertBeforeHead(K key,V data) {
        DoNode node = new DoNode(key,data);
        node.pre = sentryNode;
        node.next = sentryNode.next;
        node.next.pre = node;
        sentryNode.next = node;

        size++;
    }

    public static void main(String[] args) {
        LRUCache<Integer,Integer> cache = new LRUCache<>(3);
        Scanner in = new Scanner(System.in);
        Integer[] input = {1, 2, 3, 2, 3};
        for (Integer data : input) {
            Integer key=data-1;
            cache.push(key,data);
        }


    }


    class DoNode<K,V> {
        K key;
        V data;
        DoNode pre;
        DoNode next;

        public DoNode(K key,V data) {
            this.key=key;
            this.data = data;
        }

        public DoNode() {
        }
    }
}




其他改进思路

使用hashMap 存储key 到节点的映射,提高查询速度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值