【设计并实现一个满足 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());
    }
}

输出结果:

相关推荐
寻星探路4 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
陌上丨6 小时前
Redis的Key和Value的设计原则有哪些?
数据库·redis·缓存
你撅嘴真丑6 小时前
第九章-数字三角形
算法
曹牧6 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
uesowys7 小时前
Apache Spark算法开发指导-One-vs-Rest classifier
人工智能·算法·spark
ValhallaCoder7 小时前
hot100-二叉树I
数据结构·python·算法·二叉树
董董灿是个攻城狮7 小时前
AI 视觉连载1:像素
算法
爬山算法7 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
智驱力人工智能7 小时前
小区高空抛物AI实时预警方案 筑牢社区头顶安全的实践 高空抛物检测 高空抛物监控安装教程 高空抛物误报率优化方案 高空抛物监控案例分享
人工智能·深度学习·opencv·算法·安全·yolo·边缘计算
kfyty7257 小时前
集成 spring-ai 2.x 实践中遇到的一些问题及解决方案
java·人工智能·spring-ai