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

输出结果:

相关推荐
无心水29 分钟前
【分布式利器:腾讯TSF】10、TSF故障排查与架构评审实战:Java架构师从救火到防火的生产哲学
java·人工智能·分布式·架构·限流·分布式利器·腾讯tsf
燃于AC之乐3 小时前
我的算法修炼之路--4 ———我和算法的爱恨情仇
算法·前缀和·贪心算法·背包问题·洛谷
Boilermaker19928 小时前
[Java 并发编程] Synchronized 锁升级
java·开发语言
Cherry的跨界思维8 小时前
28、AI测试环境搭建与全栈工具实战:从本地到云平台的完整指南
java·人工智能·vue3·ai测试·ai全栈·测试全栈·ai测试全栈
MM_MS8 小时前
Halcon变量控制类型、数据类型转换、字符串格式化、元组操作
开发语言·人工智能·深度学习·算法·目标检测·计算机视觉·视觉检测
独自破碎E8 小时前
【二分法】寻找峰值
算法
alonewolf_999 小时前
JDK17新特性全面解析:从语法革新到模块化革命
java·开发语言·jvm·jdk
一嘴一个橘子9 小时前
spring-aop 的 基础使用(啥是增强类、切点、切面)- 2
java
sheji34169 小时前
【开题答辩全过程】以 中医药文化科普系统为例,包含答辩的问题和答案
java
mit6.8249 小时前
位运算|拆分贪心
算法