理解和实现一个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);
 */
相关推荐
大神薯条老师15 分钟前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
sewinger16 分钟前
区间合并算法详解
算法
XY.散人20 分钟前
初识算法 · 滑动窗口(1)
算法
韬. .41 分钟前
树和二叉树知识点大全及相关题目练习【数据结构】
数据结构·学习·算法
Word码1 小时前
数据结构:栈和队列
c语言·开发语言·数据结构·经验分享·笔记·算法
五花肉村长1 小时前
数据结构-队列
c语言·开发语言·数据结构·算法·visualstudio·编辑器
2401_857622661 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
一线青少年编程教师1 小时前
线性表三——队列queue
数据结构·c++·算法
Neituijunsir1 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
AskHarries1 小时前
如何优雅的处理NPE问题?
java·spring boot·后端