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

输出结果:

相关推荐
Fanfanaas几秒前
C++ 继承
java·开发语言·jvm·c++·学习·算法
蚰蜒螟2 分钟前
走进 Linux 内核:从 touch 命令到磁盘 inode 的完整旅程
java·linux·前端
lqqjuly3 分钟前
模型合并与融合:理论、算法与可运行实现—从损失曲面几何到多模型融合
算法
zzqssliu6 分钟前
taocarts 跨境独立站 SEO 优化实践(多语言 + 反向海淘场景)
java·javascript·php
memcpy013 分钟前
LeetCode 2144. 打折购买糖果的最小开销【贪心】
算法·leetcode·职场和发展
在繁华处13 分钟前
Java从零到熟练(十一):Spring框架入门
java·开发语言·spring
小锋java123414 分钟前
【技术专题】LangChain4j 开发Java Agent智能体 - 整合SpringBoot4
java·人工智能
飞翔中文网1 小时前
Java学习笔记之抽象类
java·笔记·学习
海盗12341 小时前
C#中PDF操作-QuestPDF页面设置与布局
java·pdf·c#
ID_180079054731 小时前
淘宝商品详情数据接口深度解析:架构、鉴权、数据结构与实战
数据结构·架构