理解和实现一个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 小时前
外包干了27天,技术退步明显。。。。。
软件测试·面试
88号技师4 小时前
2024年12月一区SCI-加权平均优化算法Weighted average algorithm-附Matlab免费代码
人工智能·算法·matlab·优化算法
IT猿手4 小时前
多目标应用(一):多目标麋鹿优化算法(MOEHO)求解10个工程应用,提供完整MATLAB代码
开发语言·人工智能·算法·机器学习·matlab
88号技师4 小时前
几款性能优秀的差分进化算法DE(SaDE、JADE,SHADE,LSHADE、LSHADE_SPACMA、LSHADE_EpSin)-附Matlab免费代码
开发语言·人工智能·算法·matlab·优化算法
2401_882727575 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
我要学编程(ಥ_ಥ)5 小时前
一文详解“二叉树中的深搜“在算法中的应用
java·数据结构·算法·leetcode·深度优先
埃菲尔铁塔_CV算法5 小时前
FTT变换Matlab代码解释及应用场景
算法
许野平6 小时前
Rust: enum 和 i32 的区别和互换
python·算法·rust·enum·i32
追逐时光者6 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
chenziang16 小时前
leetcode hot100 合并区间
算法