LRU缓存算法设计

LRU缓存算法设计

  • 概述介绍
  • 实现思路

一、概述介绍

LRU(Least Recently Used):最近最少使用,其核心思想是一个数据在一段时间内没有被访问到,那么在将来一段时间内被访问到的概率也不大,因此在存储空间满后,应当将那些最近最少使用的数据进行删除,为最新添加的数据腾出空间。

和LRU相似的算法还有FIFO(First In FIrst Out)算法,即先进先出算法,很明显完美符合了队列的特点,因此可以利用队列进行实现。

以上介绍的算法经常被用于操作系统的内存页置换,当然也可以用作缓存的设计。

二、实现思路

  • 基于数组+时间戳实现
  • 基于HashMap+双向链表实现
  • 基于LinkedHashMap实现

基于数组实现(未经笔者验证,仅供参考)

首先可以维护一个存储数据的结构体数组,结构体定义可以如下:

typedef struct LRU{
    // 键
    int key;
    // 数据
    int val;
    // 数据停留时间戳
    long timestamp;
}LRU;

// 结构体数组
LRU cache[100];

// 游标,指向当前插入数据位置
int cursor;

可以看到,为每一个数据设置了一个时间戳,初始化都是0,那么就可以根据数组中数据的时间戳进行移除。

具体的,涉及到时间戳更新的操作大体可以分为两类:

  • set,即设置/追加数据值,此时首先应当先查询是否存在该数据,不存在则先将游标范围内的数据时间戳+1,然后将游标移动一位,放入数据;若存在,将其时间戳重置为0,其他数据时间戳+1。
  • get,获取数据值,此时便可以在游标范围内进行线性查找,存在,就把对应数据的时间戳置为0,不存在直接返回null即可。

两种操作的伪代码如下:

  1. set

    void set(int key, int val) {
        // 线性查找数值为key第一次出现的下标
    	int ret = searchIndex(key);
        
        // 大于等于零表示存在
        if (ret >= 0) {
            cache[ret].val = val;
            cache[ret].timestamp = 0;
            // 更新其他数据的时间戳
            updateTimeStamp(ret);
            return;
        }
        // 小于零表示不存在
        // 更新游标范围内的数据时间戳
        updateTimeStamp();
        cache[++cursor].key = key;
        cache[cursor].val = val;
    }
    
  2. get

    int get(int key) {
        // 线性查找数值为key第一次出现的下标
        int ret = searchIndex(key);
        
        // 大于等于零表示存在
        if (ret >= 0) {
            // 重置时间戳
            cache[ret].timestamp = 0;
            return cache[ret].val;
        }
        return 0x7fff;
    }
    

通过上述伪代码可知,设置和获取数据时间复杂度达到了 O ( n ) O(n) O(n),但也总归是实现了,因此还可以做出一定的优化:

  • 将下标当作key,可以使得查询复杂度降为 O ( 1 ) O(1) O(1),但缺点是容量不是很灵活。
  • 其他优化

基于HashMap+双向链表实现

这种实现可以利用HashMap记录存储的键值以及对应的结点Node,实现查询复杂度为 O ( 1 ) O(1) O(1),并且容量也是非常灵活的;利用双向链表存储具体的键值对,双向的目的是访问某个结点的前驱结点和后继结点很方便。

同时我们还需要维护一个头指针head,一个尾指针tail。其中头指针指向最近最少使用的结点,尾指针指向最近经常被访问的结点,当然也可以倒过来?

具体的实现思路:

  • 进行set操作时,快速查询是否存在:存在,则更新该结点数据,将该结点移动到尾结点指向的位置;不存在,则构建新结点,向链表末尾添加该结点。
  • 进行get操作时,依然快速查询是否存在:存在,则将其移动到尾结点指向的位置;不存在,则返回null。

实现代码如下(力扣原题):

class LRUCache {
    
    // 容量
    int cap;

    Map<Integer, Node> map = new HashMap<>();

    // 头指针
    Node head;

    // 尾指针
    Node tail;

    // 结点--双向链表
    private class Node{
        Node pre, next;
        int key, value;

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

    public LRUCache(int capacity) {
        this.cap = capacity;
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.next = tail;
        tail.pre = head;
    }
    
    public int get(int key) {
        Node node = map.get(key);
        if (node != null) {
            removeNode(node);
            appendLast(node);
            return node.value;
        }
        return -1;
    }
    
    public void put(int key, int value) {
        Node node = map.get(key);
        if (node != null) {
            node.value = value;
            map.put(key, node);
            removeNode(node);
            appendLast(node);
            return;
        }

        // 容量满弹出最近最少使用的元素
        if (map.size() == cap) {
            removeLast();
        }
        node = new Node(key, value);
        appendLast(node);

        //===================
        map.put(key, node);
    }

    private void removeNode(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    private void removeLast() {
        //==================
        Node tmp = head.next;
        removeNode(tmp);

        //==================
        map.remove(tmp.key);
    }

    private void appendLast(Node node) {
        tail.pre.next = node;
        node.pre = tail.pre;
        tail.pre = node;
        node.next = tail;
    }
}

/**
 * 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);
 */

可以看出,插入和获取元素时间复杂度都降到了 O ( 1 ) O(1) O(1),是很不错的。

当然可以进一步优化,即只构建一个head指针即可,构造一个环形链表,head.pre指向最新访问的结点,head.next指向最近最少使用的结点。

基于LinkedHashMap实现

上述基于HashMap+双向链表其实就是LinkedHashMap的底层实现,哈哈哈哈,所以利用封装好的数据结构就行了。

public class LRUCache<K , V> {

    private int capacity;
    private Map<K, V> cache;

    public LRUCache(final int capacity) {
        this.capacity = capacity;
        this.cache = new java.util.LinkedHashMap<K, V> (capacity, 0.75f, true) {
            // 定义put后的移除规则,大于容量就删除eldest
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > capacity;
            }
        };
    }

    public V get(K key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else
            return null;
    }

    public void set(K key, V value) {
        cache.put(key, value);
    }
}

总结

设计缓存算法,本质上还是在考察数据结构以及操作系统的基本知识以及熟悉程度!

参考链接:

设计实现一个LRU Cache

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值