理解和实现一个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);
 */
相关推荐
骑自行车的码农7 分钟前
🍂 React DOM树的构建原理和算法
javascript·算法·react.js
codetown20 分钟前
openai-go通过SOCKS5代理调用外网大模型
人工智能·后端
星辞树21 分钟前
MIT 6.824 Lab 3 通关实录:从 Raft 到高可用 KV 存储
后端
CoderYanger33 分钟前
优选算法-优先级队列(堆):75.数据流中的第K大元素
java·开发语言·算法·leetcode·职场和发展·1024程序员节
希望有朝一日能如愿以偿34 分钟前
力扣每日一题:能被k整除的最小整数
数据结构·算法·leetcode
Controller-Inversion35 分钟前
力扣53最大字数组和
算法·leetcode·职场和发展
rit843249936 分钟前
基于感知节点误差的TDOA定位算法
算法
m0_3722570240 分钟前
ID3 算法为什么可以用来优化决策树
算法·决策树·机器学习
q***25211 小时前
SpringMVC 请求参数接收
前端·javascript·算法
Dream it possible!1 小时前
LeetCode 面试经典 150_图_克隆图(90_133_C++_中等)(深度优先:DFS)
c++·leetcode·面试·