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

文章目录

  • 一、什么是LRU?
  • 二、LinkedHashMap 实现LRU缓存
  • 三、手写LRU

一、什么是LRU?

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

如图所示:

在这里插入图片描述

  1. 先来3个元素进入该队列
    在这里插入图片描述

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

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

  4. 如果此时又来了新的元素,还是”咯咯“,由于”咯咯“已经处于最新的位置,所以大家位置都不变。
    在这里插入图片描述

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


二、LinkedHashMap 实现LRU缓存

以力扣的算法题为例子:


力扣146. LRU 缓存


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

在这里插入图片描述

代码示例一:

/**
 * 利用继承 LinkedHashMap 的方式实现LRU缓存
 */


class LRUCache extends LinkedHashMap<Integer, Integer> {
	// 缓存容量
    private int capacity;

    public LRUCache(int capacity) {
    	// 初始化
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
    	// 重写比较方法
        return super.size() > capacity;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

代码示例二:

/**
 * 利用 LinkedHashMap 特点的方式实现LRU缓存
 */
 
class LRUCache {
   // 额定容量
    private final int CAPACITY;
    // 使用 LinkedHashMap 的有序排重特点达到要求
    private final Map<Integer, Integer> map;


    public LRUCache(int capacity) {
        // 初始化
        CAPACITY = capacity;
        map = new LinkedHashMap<>(CAPACITY);
    }

    public int get(int key) {
        Integer value = map.get(key);
        if (value != null) {// 存在
            // 1. 先删除旧的
            map.remove(key);
            // 2. 再添加回去,同时更新了 key 的位置和 value
            map.put(key, value);
            // 3. 返回结果
            return value;
        } else {// 不存在
            return -1;
        }
    }

    public void put(int key, int value) {
        if (map.size() == CAPACITY) {// 达到最大容量
            if (!map.containsKey(key)) {// 不存在,就删除头
                Integer head = map.keySet().iterator().next();
                map.remove(head);
            } else {// 存在,就删除自身
                map.remove(key);
            }
        } else {// 还有剩余空间,删除自身
            map.remove(key);
        }
        // 添加新的或 key 更新后的 value
        map.put(key, value);
    }
}



三、手写LRU

代码示例如下:

/**
 * https://leetcode.cn/problems/lru-cache/description/
 * 手写 LRU 缓存
 * Map + 双向链表
 */
class LRUCache {
    // 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 LRUCache(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);
        }
    }
}