面试官让我设计个LRU缓存,结果...

本文介绍了面试中常见的LRU缓存设计问题,详细解释了LRU缓存的淘汰策略,即最近最少使用原则,并探讨了如何利用HashMap和双向链表实现一个满足O(1)时间复杂度的LRU缓存结构。文章通过代码展示了put和get操作的实现,并讨论了如何通过读写锁实现并发安全性。最后,文章提及了LRU缓存的局限性和其他缓存淘汰策略。
摘要由CSDN通过智能技术生成

小黑有个朋友最近去面试,过程中有问他一些缓存相关的问题。

让他回答一下,设计一个LRU缓存,应该怎么实现

我这个朋友呢,应该是没好好准备这块儿内容,反正是没咋答上来,于是。。。就让他回家等通知了。

今天小黑就带大家来聊一聊LRU算法,并动手写一个LRU缓存。

缓存淘汰策略是啥

在我们平时开发中,经常会使用到缓存,比如一些热点商品,配置数据等,为了提高访问速度都会放到缓存中,但是,往往缓存的容量是有限的,我们不能将所有数据都放在缓存中,需要给缓存设定一个容量,当容量放满之后,要有新的数据放入缓存时,需要按照一定的策略将原来缓存中的数据淘汰掉,那么这个策略就叫做缓存淘汰策略

缓存淘汰策略有很多种选择,常见的有FIFO(first in last out),LRU(Least Recently Used),LFU(Least Frequently Used)等。

LRU是啥

LRU是Least Recently Used的缩写,意思是最近最少使用,也就是说将最近使用最少的数据淘汰掉。

例如,我们有如下一个缓存结构:

最开始缓存时空的,我们分别往缓存中放入了5,6,9三个元素,接着在放元素3时,缓存空间已经使用完了,这是我们需要淘汰掉一个元素,释放出空间放心的元素,按照LRU算法的逻辑,此时缓存中最近最少使用的元素是5,所以将5淘汰掉,放入元素3。

接下来我们来想想如何实现这样一个LRU缓存结构,在开始写代码之前,我们要先明确我们这个缓存需要满足的一个条件。

  • 该缓存的容量要有限制
  • 在缓存容量使用完时,再添加新元素时必须使用LRU算法淘汰元素
  • 添加元素,查询元素操作的时间复杂度都应该是O(1)
  • 对缓存的操作要支持并发

因为有上面的一些要求,我们先来思考下面这个问题。

如何保证所有操作的时间复杂度都是O(1)呢?

要找到这个问题的答案,我们还得再深入思考一下LRU缓存的特点。

首先,按照开头我们图片看,LRU缓存应该是一个队列结构,如果一个元素被重新访问,那么这个元素要重新放到队列的头部;

然后呢,我们的队列有容量限制,每当有新元素要添加进缓存时,都把它添加到队列的头部;有元素要淘汰时,都从队列尾部删除;

这样可以保证添加和淘汰元素的时间复杂度都是O(1),那么我们如果想从缓存中查询元素时,该怎么办呢?

你是不是想到了将队列遍历一遍,找到符合的元素?很显然这样是能找到元素,但是时间复杂度是O(n)的,并且当我们缓存中数据量如果很多时,查询元素的时间是不固定的,可能会很快,可能会特别慢。

如果单纯使用队列的话,是做不到查询操作的时间复杂度为O(1)的。

怎样可以让查询的时间复杂度变成O(1)呢?

队列不可以,但是HashMap可以呀。

但是问题又来了,如果仅使用HashMap,虽然可以让查询时间复杂度变为O(1),但是淘汰元素时,就没办法用O(1)的时间复杂度删除了

我们可以使用HashMap+链表的组合方式,来完成LRU缓存的结构。

以上结构,我们可以把要缓存数据的key做为HashMap的key,这样保证查询元素时能快速查到数据;

通过双向链表,可以保证在添加新元素和淘汰元素时从头节点和尾节点操作,可能在O(1)时间内完成。

现在是不是思路变得非常清晰了!

好的,我们现在来写一下实现思路:

首先,如果HashMap中包含key,则缓存命中,获取元素;如果不包含,则表示缓存未命中。

如果缓存命中:

  • 从链表中移除该元素,将元素添加到链表头部;
  • 将头部节点作为value保存到HashMap中;

如果未命中:

  • 添加该元素到链表头部;
  • 将链表头部节点保存在HashMap中。
  • 嗯,到这里我们就可以写代码了。

代码实现

首先我们定义一个Cache接口,在该结构中定义Cache有哪些方法:

/**
 * @author 小黑说Java
 * @ClassName Cache
 * @Description
 * @date 2022/1/13
 **/
public interface Cache<K, V> {
   
    
    boolean put(K key, V value);

    Optional<V> get(K key);

    int size();

    boolean isEmpty();

    void clear();
}

接下来,我们来实现我们的LRUCache类,该类实现Cache接口:

public class LRUCache<K, V> implements Cache<K, V> {
   
    private int size;
    private Map<K, LinkedListNode<CacheElement<K, V>>> linkedListNodeMap;
    private DoublyLinkedList<CacheElement<K, V>> doublyLinkedList;

    public LRUCache(int size) {
   
        this.size = size;
        this.linkedListNodeMap = new ConcurrentHashMap<>(size);
        this.doublyLinkedList = new DoublyLinkedList<>();
    }
    // ...其他方法
}

首先我们在LRUCache中定义了一个Map和我们自定义的双向链表DoublyLinkedList,在构造方法中进行初始化。

接下来实现具体操作的方法。

put操作


                
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小黑说Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值