深度解析:146. LRU 缓存(哈希表 + 双向链表的艺术)

LRU (Least Recently Used) 的核心在于:"如果一个数据最近被访问过,那么它在未来被访问的概率也更高。"

为了达到 的操作效率,我们需要两种数据结构的结合:

  1. 哈希表 (Map) :提供 的随机访问能力,通过 key 快速定位节点。
  2. 双向链表 (Doubly Linked List):提供 的节点移动能力,用来维护访问顺序。

实现细节优化

1. 定义双向链表节点

使用双向链表是因为删除一个节点时,我们需要知道它的前驱节点。

javascript 复制代码
class ListNode {
    constructor(key, value) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}
2. LRU 缓存类定义

技巧: 使用"哨兵节点"(Dummy Head/Tail)可以极大简化边界条件的处理,避免判断 null

javascript 复制代码
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.map = new Map();
        
        // 初始化哨兵节点,它们不存储实际数据
        this.head = new ListNode(-1, -1);
        this.tail = new ListNode(-1, -1);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

关键内部方法(封装逻辑)

将复杂的链表操作封装为私有方法,使 getput 的主逻辑像英语一样易读。

javascript 复制代码
    // 将节点移至头部(表示最近使用)
    moveToHead(node) {
        this.removeNode(node);
        this.addToHead(node);
    }

    // 从链表中彻底断开节点
    removeNode(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 在哨兵头节点后插入新节点
    addToHead(node) {
        node.prev = this.head;
        node.next = this.head.next;
        this.head.next.prev = node;
        this.head.next = node;
    }

    // 弹出最久未使用的节点(尾部哨兵前的一个)
    removeTail() {
        const lastNode = this.tail.prev;
        this.removeNode(lastNode);
        return lastNode; // 返回节点以便从 map 中删除
    }

主接口实现

javascript 复制代码
    /** * @param {number} key
     * @return {number}
     */
    get(key) {
        if (!this.map.has(key)) return -1;
        
        const node = this.map.get(key);
        this.moveToHead(node); // 刷新位置
        return node.value;
    }

    /** * @param {number} key 
     * @param {number} value
     */
    put(key, value) {
        if (this.map.has(key)) {
            const node = this.map.get(key);
            node.value = value; // 更新值
            this.moveToHead(node);
        } else {
            const newNode = new ListNode(key, value);
            this.map.set(key, newNode);
            this.addToHead(newNode);
            
            if (this.map.size > this.capacity) {
                const removed = this.removeTail();
                this.map.delete(removed.key); // 必须在节点中存储 key 才能在此删除 map 记录
            }
        }
    }
}
相关推荐
玖玥拾8 小时前
LeetCode 219 存在重复元素 II
算法·leetcode·哈希算法·散列表
努力努力再努力wz10 小时前
【Redis入门系列】从 KEYS 到 SCAN:渐进式遍历、Cursor 与位反转原理
数据库·redis·缓存
小宋102111 小时前
大模型成本怎么控制:Token统计、缓存与模型路由实战
人工智能·缓存
啵啵啵鱼13 小时前
DS-顺序表
java·数据结构·笔记·后端·链表·obsidian
洋不写bug14 小时前
链表面试笔试经典题目详细解析,题目多解法,复杂度分析
数据结构·链表·面试
宿67417 小时前
vue3-时间戳防缓存
缓存
Cccp.12318 小时前
【leetcode】(四)排序算法汇总和链表
leetcode·链表·排序算法
三84421 小时前
SSRF 从入门到实战(下篇):gopher 打 Redis 实现 RCE
数据库·redis·缓存
名字还没想好☜21 小时前
Java 用 LinkedHashMap 三行实现 LRU 缓存:accessOrder、removeEldestEntry 与线程安全
java·后端·安全·spring·缓存
手握风云-21 小时前
Redis:不只是缓存那么简单(十六)
缓存