Hot100(开刷) 之 LRU(最近最少使用)缓存

LRU介绍

LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略。它的核心思想非常符合直觉:如果一个数据在最近一段时间没有被访问过,那么它在将来被访问的可能性也很小 。因此,当缓存容量达到上限时,系统会优先淘汰那个"最长时间未被使用"的数据。

为了让你更直观地理解,我们可以把 LRU 缓存想象成一个容量固定的书架

  • 规则:书架只能放 N 本书。
  • 取书/放书 :每当你阅读或放入一本书,就把这本书放到书架的最右侧(代表"最近刚用过")。
  • 淘汰机制 :当书架满了,而你需要放入一本新书时,必须把书架最左侧那本"最久没碰过"的书扔掉,腾出空间给新书。

相关代码

数据结构LinkedHashMap实现

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache {

    // 缓存的最大容量
    private final int capacity;
    
    // 核心容器:使用 LinkedHashMap 存储键值对
    // LinkedHashMap 底层是"哈希表 + 双向链表",默认按照"插入顺序"维护元素
    // 这里的技巧是:通过"先删后插"的操作,手动把被访问的元素挪到链表末尾
    private final Map<Integer, Integer> cache = new LinkedHashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    
    public int get(int key) {
        // 核心动作:尝试从 map 中移除 key
        // remove(key) 会返回被删除的 value;如果 key 不存在,返回 null
        // 这一步不仅获取了值,还顺带把该节点从双向链表中"摘除"了
        Integer value = cache.remove(key);
        
        if (value != null) {
            // 如果 value 不为 null,说明 key 存在(缓存命中)
            // 重新执行 put 操作。因为是新插入,LinkedHashMap 会把这个元素放到链表的【最末尾】
            // 这就实现了"刷新热度",将其标记为"最近刚用过"
            cache.put(key, value);
            return value;
        }
        
        // key 不在 cache 中,直接返回 -1
        return -1;
    }
    
    public void put(int key, int value) {
        // 同样先尝试移除旧节点。如果返回值不为 null,说明是"更新操作"
        if (cache.remove(key) != null) {
            // 更新值并重新插入到链表末尾(刷新热度)
            // 因为只是更新,缓存大小没变,不需要判断容量,直接返回即可
            cache.put(key, value);
            return;
        }
        
        // 执行到这里,说明 key 之前不存在,是一个"全新插入"的操作
        // 插入前必须先判断缓存是否已满
        if (cache.size() == capacity) {
            // cache 满了,需要淘汰"最久未使用"的元素
            // 在 LinkedHashMap 的插入顺序模式下,迭代器的【第一个元素】就是最早插入/最久未更新的
            // 也就是链表【头部】的节点,即我们要淘汰的 LRU 节点
            Integer eldestKey = cache.keySet().iterator().next();
            
            // 删除链表头部的这个最旧节点,腾出空间
            cache.remove(eldestKey);
        }
        
        // 将新节点插入到链表【末尾】,标记为最新使用
        cache.put(key, value);
    }
}

手撕双向链表实现

java 复制代码
class LRUCache {
    private static class Node {
        int key, value;
        Node prev, next;

        Node(int k, int v) {
            key = k;
            value = v;
        }
    }

    private final int capacity;
    private final Node dummy = new Node(0, 0); // 哨兵节点
    private final Map<Integer, Node> keyToNode = new HashMap<>();

    public LRUCache(int capacity) {
        this.capacity = capacity;
        dummy.prev = dummy;
        dummy.next = dummy;
    }

    public int get(int key) {
        Node node = getNode(key); // getNode 会把对应节点移到链表头部
        return node != null ? node.value : -1;
    }

    public void put(int key, int value) {
        Node node = getNode(key); // getNode 会把对应节点移到链表头部
        if (node != null) { // 有这本书
            node.value = value; // 更新 value
            return;
        }
        node = new Node(key, value); // 新书
        keyToNode.put(key, node);
        pushFront(node); // 放到最上面
        if (keyToNode.size() > capacity) { // 书太多了
            Node backNode = dummy.prev;
            keyToNode.remove(backNode.key);
            remove(backNode); // 去掉最后一本书
        }
    }

    // 获取 key 对应的节点,同时把该节点移到链表头部
    private Node getNode(int key) {
        if (!keyToNode.containsKey(key)) { // 没有这本书
            return null;
        }
        Node node = keyToNode.get(key); // 有这本书
        remove(node); // 把这本书抽出来
        pushFront(node); // 放到最上面
        return node;
    }

    // 删除一个节点(抽出一本书)
    private void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
    }

    // 在链表头添加一个节点(把一本书放到最上面)
    private void pushFront(Node x) {
        x.prev = dummy;
        x.next = dummy.next;
        x.prev.next = x;
        x.next.prev = x;
    }
}

常用函数

常用函数 基础功能 在 LRU 中的特殊作用
remove(Object key) 删除指定 key 的映射,返回对应的 value;若 key 不存在则返回 null 实现"移动节点"的核心。通过先 removeput,利用 LinkedHashMap 的插入顺序特性,手动把被访问的元素从链表中间"挪"到了链表末尾,从而刷新其热度。
put(K key, V value) 插入或更新键值对。如果 key 已存在则覆盖 value;如果 key 不存在则插入到链表末尾。 标记"最近使用"。无论是 get 命中后的重新插入,还是全新的 put 操作,都会把元素放到链表的最尾部,代表它是当前最新的数据。
keySet().iterator().next() 获取 Map 中所有 key 的集合,并拿到迭代器的第一个元素。 精准定位"淘汰目标"。在默认的插入顺序模式下,迭代器的第一个元素永远是最早被插入且后续未被更新过的节点,也就是链表头部的"最久未使用"元素。
size() 返回 Map 中当前存储的键值对数量。 容量守门员。在插入新元素前判断 size() == capacity,确保缓存不会超过预设上限,触发淘汰逻辑。
相关推荐
玛卡巴卡ldf1 天前
【LeetCode 手撕算法】(多维动态规划)不同路径、最小路径和、最长回文子串、最长公共子序列、编辑距离
java·数据结构·算法·leetcode·动态规划·力扣
旖-旎4 天前
深搜练习(单词搜索)(12)
c++·算法·深度优先·力扣
玛卡巴卡ldf4 天前
【LeetCode 手撕算法】(动态规划)爬楼梯、杨辉三角、打家劫舍、完全平方数、零钱兑换、单词拆分、最长递增子序列、乘积最大子数组、分割等和子集
java·数据结构·算法·leetcode·动态规划·力扣
玛卡巴卡ldf7 天前
【LeetCode 手撕算法】(栈)有效括号、最小栈、字符串解码、每日温度、柱状图最大矩形
java·数据结构·算法·leetcode·力扣
小辉同志10 天前
62. 不同路径
c++·力扣·多维动态规划
玛卡巴卡ldf10 天前
【LeetCode 手撕算法】(回溯)全排列DFS、子集、电话号码字母组合 九键、组合总和、括号生成、单词搜索、分割回文数
java·算法·leetcode·力扣
mask哥11 天前
15种算法模式java实现详解
java·算法·力扣
旖-旎15 天前
深搜练习(N皇后)(10)
c++·算法·深度优先·力扣
加农炮手Jinx16 天前
LeetCode 26. Remove Duplicates from Sorted Array 题解
算法·leetcode·力扣