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

输出结果:

相关推荐
奶糖趣多多36 分钟前
Redis知识点
数据库·redis·缓存
阿伟*rui36 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
浮生如梦_2 小时前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
CoderIsArt2 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei3 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法