理解和实现一个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);
 */
相关推荐
专注VB编程开发20年4 分钟前
asp.net mvc如何简化控制器逻辑
后端·asp.net·mvc
GalaxyPokemon27 分钟前
LeetCode - 148. 排序链表
linux·算法·leetcode
用户67570498850235 分钟前
告别数据库瓶颈!用这个技巧让你的程序跑得飞快!
后端
iceslime1 小时前
旅行商问题(TSP)的 C++ 动态规划解法教学攻略
数据结构·c++·算法·算法设计与分析
spionbo1 小时前
Vue 表情包输入组件实现代码及完整开发流程解析
前端·javascript·面试
千|寻1 小时前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
天涯学馆1 小时前
前后端分离的 API 设计:技术深度剖析
前端·javascript·面试
程序员岳焱1 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯1 小时前
Node.js 模块导入语法变革全解析
后端·node.js