LRU缓存算法

677 阅读3分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

前言

笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。

系列文章收录《算法》专栏中。

LRU算法还是比较常见的,比如redis内存空间有限会自动清理最近最少使用的数据,还有mybatis的本地缓存也有LruCaches实现类。

问题描述

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

  • 1 <= capacity <= 3000
  • 0 <= key <= 10000
  • 0 <= value <= 105
  • 最多调用 2 * 10^5 次 get 和 put

确定学习目标

对《LRU缓存算法》的算法过程进行剖析。

剖析

我们对问题的要求一一想办法解决:

  1. LRUCache对象创建的时候需要设置capacity用于控制容量:这个比较简单,LRUCache配置成员capacity(进行设置容量)、size(记录目前存放的元素的个数),在put的时候需要比较capacity和size的大小。
  2. int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1:这个也不困难但是get的平均时间复杂度需要控制在O(1),我们很容易相当实用HashMap去存放,就算hash碰撞也只要遍历链表和走红黑。
  3. void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字:前半部分HashMap都已经实现,后半部分我们和get放在第四点进行描述。
  4. get之后对应元素变成最近最多实用的、put之后对应元素变成最近最多实用的、put超过capacity之后需要淘汰最久未使用的关键字:我们也比较容易想到是不是可以使用双向队列,刚get或者put的元素放在队列头部,超过capacity之后直接淘汰队列尾部的。

下面直接上代码

代码

package com.study.algorithm.lru;

import java.util.HashMap;
import java.util.Map;

public class LRUCache {
    /**
     * 双向列表节点
     */
    class DLinkedNode {
        private int key;
        private int value;
        /**
         * 节点前面一个节点
         */

        private DLinkedNode prev;
        /**
         * 节点下一个节点
         */

        private DLinkedNode next;

        public DLinkedNode() {
        }

        public DLinkedNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    /**
     * 使用hashmap作为缓存,key为key,value为DLinkedNode
     */
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    /**
     * 当前cache大小
     */
    private int size;
    /**
     * cache容量
     */
    private int capacity;
    /**
     * 头部,cache实例化的时候就进行初始化,固定有头部
     */
    private DLinkedNode head;
    /**
     * 尾部,cache实例化的时候就进行初始化,固定有尾部
     */
    private DLinkedNode tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.head = new DLinkedNode();
        this.tail = new DLinkedNode();
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    /**
     * 根据key获得,没有返回-1,有返回value,并且把对应节点移动到头部
     *
     * @param key
     * @return
     */
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        //从链表中删除该节点并移动到头部
        moveToHead(node);
        return node.value;
    }

    /**
     * 有就重新赋值value并移动到头部,没有就添加。
     * 需要注意判断size是否大于等于capacity,true的话就从cache和链表中移除,false的话就添加cache并且向头部添加该node
     *
     * @param key
     * @param value
     */
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            //如果cache的大小超过容量就删除
            if (size >= capacity) {
                cache.remove(tail.prev.key);
                removeNode(tail.prev);
                --size;
            }
            DLinkedNode newNode = new DLinkedNode(key, value);
            addToHead(newNode);
            cache.put(key, newNode);
            ++size;
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

    /**
     * 从链表中去除该节点
     *
     * @param node
     */
    public void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    /**
     * 把该节点增加到头部
     *
     * @param node
     */
    public void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    /**
     * 从链表中删除该节点并移动到头部
     *
     * @param node
     */
    public void moveToHead(DLinkedNode node) {
        //先删除
        removeNode(node);
        //后增加到头部
        addToHead(node);
    }

    public static void main(String[] args) {
        LRUCache lRUCache = new LRUCache(2);
        lRUCache.put(1, 1); // 缓存是 {1=1}
        lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
        lRUCache.get(1);    // 返回 1
        lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
        lRUCache.get(2);    // 返回 -1 (未找到)
        lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
        lRUCache.get(1);    // 返回 -1 (未找到)
        lRUCache.get(3);    // 返回 3
        lRUCache.get(4);    // 返回 4
    }
}