【设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构】

文章目录


一、什么是LRU?

LRU是Least Recently Used的缩写,意为最近最少使用。它是一种缓存淘汰策略,用于在缓存满时确定要被替换的数据块。LRU算法认为,最近被访问的数据在将来被访问的概率更高,因此它会优先淘汰最近最少被使用的数据块,以给新的数据块腾出空间。

如图所示:

  1. 先来3个元素进入该队列

  2. 此时来了新的元素,因为此时队列中每个元素的使用的次数都相同(都是1),所以会按照LFU的策略淘汰(即淘汰掉最老的那个)

  3. 此时又来了新的元素,而且是队列是已经存在的,就会将该元素调整为最新的位置。

  4. 如果此时又来了新的元素,还是"咯咯",由于"咯咯"已经处于最新的位置,所以大家位置都不变。

  5. 同理,一直进行上述的循环


二、LinkedHashMap 实现LRU缓存

在官方的介绍中可以看出,该数据结构天生适合实现LRU。

代码示例:

java 复制代码
/**
 * 利用 LinkedHashMap 实现LRU缓存(该数据结构天生适合实现 LRU)
 * @param <K>
 * @param <V>
 */
public class LRUCache1<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;// 缓存坑位

    public LRUCache1(int capacity) {
        // 构造函数中传入了LinkedHashMap的构造函数参数
        // true 表示更改存储顺序(LRU),false 表示不更改存储顺序(不是真正的 LRU)
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 指定规则,当缓存满时,移除最老的缓存项(实现LRU的关键)
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        // 容量为 3
        LRUCache1<String, String> lruCache1 = new LRUCache1<>(3);
        // 每一次put,就相当于刷新或插入一次元素
        lruCache1.put("1", "1");
        lruCache1.put("2", "2");
        lruCache1.put("3", "3");
        // 此时队列已满
        System.out.println("队列已满,如下");
        System.out.println(lruCache1.keySet());
        // 继续插入新的元素
        lruCache1.put("4", "4");
        System.out.println("插入新元素 4");
        System.out.println(lruCache1.keySet());
        // 刷新已经存在的元素
        lruCache1.put("3", "3");
        System.out.println("已经存在的元素 3");
        System.out.println(lruCache1.keySet());
        // 刷新已经存在的元素
        lruCache1.put("3", "3");
        System.out.println("已经存在的元素 3");
        System.out.println(lruCache1.keySet());
        // 继续插入新的元素
        lruCache1.put("5", "5");
        System.out.println("插入新元素 5");
        System.out.println(lruCache1.keySet());
    }
}

输出结果:


三、手写LRU

以力扣的算法题为例子:

力扣146. LRU 缓存

代码示例如下:

java 复制代码
/**
 * https://leetcode.cn/problems/lru-cache/description/
 * 手写 LRU 缓存
 * Map + 双向链表
 */
public class LRUCache2 {
    // Map 负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个 Node 节点,作为数据载体。

    // 参考 HashMap 中的存储方式,使用内部 Node 维护双向链表
    // 1. 构造 Node 节点作为数据载体
    public static class Node<K, V> {
        public K key;
        public V value;
        public Node<K, V> prev;
        public Node<K, V> next;

        public Node() {
            this.prev = this.next = null;
        }

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.prev = this.next = null;
        }
    }

    // 2. 构造一个双向队列,里面存放着 Node 节点
    // 队头元素最新,队尾元素最旧
    static class DoubleLinkedList<K, V> {
        Node<K, V> head;
        Node<K, V> tail;

        // 2.1 构造方法
        public DoubleLinkedList() {
            head = new Node<>();
            tail = new Node<>();
            // 头尾相连
            head.next = tail;
            tail.prev = head;
        }

        // 2.2 添加到头(头插)
        public void addHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        // 2.3 删除节点
        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.next = null;
            node.prev = null;
        }

        // 2.4 获取最后一个节点
        public Node getLast() {
            return tail.prev;
        }
    }

    private final int cacheSize;// 缓存容量
    Map<Integer, Node<Integer, Integer>> map;// Map
    DoubleLinkedList<Integer, Integer> doubleLinkedList;// 手动实现的双向链表


    public LRUCache2(int capacity) {
        this.cacheSize = capacity;
        map = new HashMap<>();
        doubleLinkedList = new DoubleLinkedList<>();
    }


    public int get(int key) {
        if (map.containsKey(key)) {
            // 命中,更新双向链表
            Node<Integer, Integer> node = map.get(key);
            // 先删除双向链表中的节点
            doubleLinkedList.removeNode(node);
            // 再添加到头部
            doubleLinkedList.addHead(node);
            return node.value;
        } else {
            // 未命中,返回 -1
            return -1;
        }
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            // 更新双向链表
            Node<Integer, Integer> node = map.get(key);
            // 新值替换老值,再放回
            node.value = value;
            map.put(key, node);
            // 先删除双向链表中的节点
            doubleLinkedList.removeNode(node);
            // 再添加到头部
            doubleLinkedList.addHead(node);
        } else {
            if (map.size() == cacheSize) {
                // 超出容量,删除双向链表的最后一个节点
                Node lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }
            // 新增节点
            Node<Integer, Integer> newNode = new Node<>(key, value);
            map.put(key, newNode);
            doubleLinkedList.addHead(newNode);
        }
    }

    /**
     * map 没有顺序,所以需要遍历双向链表,来确定是否是 LRU
     * @return
     */
    public List<Integer> getAllKeys() {
        Node<Integer, Integer> cur = doubleLinkedList.head.next;
        List<Integer> ret = new LinkedList<>();
        while (cur != doubleLinkedList.tail) {
            ret.add(cur.key);
            cur = cur.next;
        }
        return ret;
    }

    public static void main(String[] args) {
        LRUCache2 lruCache2 = new LRUCache2(3);
        lruCache2.put(1, 1);
        lruCache2.put(2, 2);
        lruCache2.put(3, 3);
        System.out.println(lruCache2.getAllKeys());

        lruCache2.put(4, 4);
        System.out.println(lruCache2.getAllKeys());

        lruCache2.put(3, 3);
        System.out.println(lruCache2.getAllKeys());
    }
}

输出结果:

相关推荐
Dlwyz1 小时前
redis-击穿、穿透、雪崩
数据库·redis·缓存
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
冰帝海岸3 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象3 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了4 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·4 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王4 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康4 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud