理解和实现一个LRU算法

什么是LRU算法

LRU,全称Least Recently Used,即最近最少使用

常被用于缓存淘汰策略,核心思想就是依据缓存的访问时间来降序排序。 最新被访问的缓存排前面,当需要删除缓存时,优先删除排在末尾的缓存

假设现在有一个容量大小为4的LRU列表,对LRU列表的操作主要有两类:

  • 写入缓存
  • 读取缓存

写入缓存

当写入的缓存不存在时

当写入的缓存不存在时,直接添加到列表头节点位置。当列表长度超过限制时,删除列表的尾节点

依次添加A、B、C、D四个缓存,结果如下图:

添加缓存E,此时列表长度超过4,需要删除尾节点A,结果如下图:

当写入的缓存已存在时

当写入的缓存已存在时,更新对应缓存的值,把对应的节点移到头节点即可。因为缓存已存在,列表的大小不会改变,所以不需要进行删除的操作

写入已存在的缓存B列表前后变化,如下图:

读取缓存

当读取的缓存不存在时,直接返回null(或其他默认值)即可

当读取的缓存存在时,除了返回对应的缓存,还需要把缓存移动到头节点。读取不涉及列表大小的改变,所以也不需要考虑删除操作

读取已存在的缓存B列表前后变化,如下图:

如何实现一个LRU算法

设计思路

LRU列表的主要操作:

  • 写入缓存
  • 读取缓存

两种操作都会频繁涉及到节点的新增和删除(节点的移动,其实也可以通过先删除,后新增来实现),因此使用链表比数组更加适合

我们经常需要在链表头部添加节点(写入/读取已存在的缓存时,需要把对应节点添加到头节点),在链表尾部删除节点(LRU列表超过最大长度限制时),因此快速定位链表的头尾节点也是一个需要解决的问题

如何快速定位头节点?可以通过添加虚拟头节点来解决,通过head.next快速定位

如何快速定位尾节点?通过添加虚拟尾节点 + 双向链表来解决,通过tail.prev来快速定位

另外我们还需要读取缓存,如果只使用双向链表的话,需要循环遍历获取,时间复杂度为O(N),为了降低读取的时间复杂度,可以使用哈希表来进一步优化

代码

核心思想:双向链表 + 哈希表

LeetCode 146. LRU 缓存

ini 复制代码
class LRUCache {
    // 定义链表节点
    class MLinkedNode {
       int key;
       int value;
       MLinkedNode prev;
       MLinkedNode next; 

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

    // 链表节点数
    private int size;
    // 链表最大容量
    private int cap;
    // 链表节点哈希表
    private Map<Integer, MLinkedNode> nodeMap;
    // 虚拟头尾节点
    private MLinkedNode head;
    private MLinkedNode tail;

    public LRUCache(int capacity) {
        cap = capacity;
        nodeMap = new HashMap<Integer, MLinkedNode>();
        head = new MLinkedNode();
        tail = new MLinkedNode();
        // 形成双向链表
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        MLinkedNode node = nodeMap.get(key);
        if (node == null) {
            return -1;
        }

        // 把对应的key移动到最前面
        // 先删除
        removeNode(node);
        // 后新增
        addNodeToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        MLinkedNode node = nodeMap.get(key);
        if (node == null) {
            // 写入的缓存不存在
            node = new MLinkedNode(key, value);
            // 添加到链表头节点
            addNodeToHead(node);
            // 添加到哈希表
            nodeMap.put(key, node);
            // 链表长度+1
            size++;
            // 判断链表长度是否超过最大限制
            if (size > cap) {
                // 删除链表尾节点
                MLinkedNode last = tail.prev;
                removeNode(last);
                // 链表长度-1
                size--;
                // 从哈希表中删除
                nodeMap.remove(last.key);
            }
        } else {
            // 写入的缓存已存在
            // 更新缓存的值
            node.value = value;
            // 把对应的key移动到最前面
            // 先删除
            removeNode(node);
            // 后新增
            addNodeToHead(node);
        }
    }

    private void addNodeToHead(MLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(MLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
}

/**
 * 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);
 */
相关推荐
大侠归来4 分钟前
C 语言 | 在函数中操作数组
c语言·c++·算法
怕浪猫5 小时前
GEO 优化到底是什么?AI时代内容创作者必须懂的新技能
算法·面试·github
朝朝辞暮i6 小时前
C++ 第 23 课:class —— 开始真正进入面向对象
开发语言·c++·算法
Evand J7 小时前
【MATLAB例程】三维RRT+APF避障路径规划与到达角(AOA)定位算法|三维路径优化与定位仿真例程
算法·matlab·路径规划·代码·定位·rrt·aoa
明月_清风7 小时前
Muse 登顶 App Store 第一,SDK 直接开源:AI Agent 开始进入下一个阶段
人工智能·后端
weixin_307779138 小时前
有限产能智能排产与动态重排智能体:从需求解构到技术实现
开发语言·人工智能·算法·架构
朝朝辞暮i8 小时前
C++ 第 27 课:智能指针 shared_ptr
开发语言·c++·算法
朝朝辞暮i9 小时前
C++ 第 29 课:回调函数 Callback
开发语言·c++·算法
Godspeed Zhao9 小时前
现代智能雷达技术14——算法(0)
线性代数·算法·雷达
可乐ea9 小时前
Agent 模型分级升级路由:用置信度阈值把贵模型省下来
人工智能·算法·机器学习·结构化输出·置信度路由·模型升级路由·大模型成本优化