必备代码(一):四种LRU缓存实现+LFU缓存实现

前言

如果面试时考察的代码中,那么LRU缓存出现的频率是非常高的。博主在LeetCode中刷这道题已经不下十次了。已经总结出了该算法的思路。

LRU

附上LRU缓存机制在LeetCode上的地址

LRU的思想:将最近一次使用的时间作为衡量某个资源(如内存)的价值,最近一次使用的时间越久,它的价值就越小。

实现LRU需要一个数据结构充当“缓存”,还需要一个数据结构用于保存实际的数据。
LRU用法很广,操作系统通常使用LRU算法作为页面置换算法,进行管理内存。

在地址映射的过程中,如果发现页面不再内存中,会产生缺页中断。如果此时内存中已经没有足够位置放入将被调入的页面,将根据页面置换算法选取一个页面换出内存。

LRU缓存就是是使用LRU算法作为缓存数据淘汰策略的缓存机制,如果了解mysql的缓冲池技术,就可以知道:mysql的数据页第一次从磁盘被调入内后,会保存在内存中的buffer pool中,buffer pool的数据页通过LRU算法管理(通过LRU链表维护),当buffer pool空间不够用的时候,最久未被访问的数据页将被换出buffer pool,同时将脏数据刷新到磁盘中。

缓存的目的是加速读,弥补速度差带来的资源浪费(CPU计算很快,远远快于从内存读取数据的过程,因此CPU高速缓存就是用来加速CPU读取数据的)。缓存的数据应该是不常改变的,对于改变的数据,可能会产生数据不一致的问题。缓存一致性可以通过指定正确的缓存写策略来解决。
如mysql允许直接对buffer pool的页进行写操作,这可能造成内存与磁盘中的数据不一致,脏页会定期刷新到磁盘中(这里不暂且考虑崩溃的问题),这使得数据页有两种状态:如果可以在内存中读取,那么它一定是最新数据,如果内存中没有数据页,那么磁盘中一定是最新数据。

缓冲的目的堆积数据,将多次小数据的读写转换成一次大数据的读写,减少IO次数/减少读写的成本。

缓存往往是数据的副本,而缓冲往往不是数据的副本。缓存考虑的是持久化和一致性问题。而缓冲考虑的是读写的时机(“发送缓冲达到多少个字节发出去”或“接收缓冲达到多少个字节取出来”)

在业务中的,也经常使用LRU算法作为淘汰策略,如主页为用户提供固定个数历史记录,如果有新的记录被添加,那么最久未被访问的记录将被移除。

HashMap + LinkedList

这是四种解法中平均耗时较长的一个,因为本实现中涉及频繁的删除和插入操作

    LinkedList<K> store = new LinkedList<>();
    HashMap<K,V> cache = new HashMap<>();

存储的数据结构就暂时命名为store,而缓存的数据结构命名为cache。

    public V get(K key) {
   
        V v = cache.get(key);
        if(v!=null){
   
            store.remove(key);
            store.add(key);
        }
        return v;
    }
    public V put(K key, V value) {
   
        V res = cache.put(key, value);
        if(res!=null){
   
            store.remove(key); 如果涉及remove调用,则为O(n)
        }
            store.add(key);
            if(store.size()>this.maxSize) 换出操作
                cache.remove(store.removeLast());

        return res;
    }

LRU的实现要点基本就是两方面:
【1】在链表中维护元素的访问顺序
【2】当链表达到上限时,换出的最久未被访问的元素

hashMap+linkedList是JDK提供好的数据结构,我们无法操作其内部的节点,只能根据对外暴露的接口去从逻辑上实现LRU。
get和put的实现思路基本一致,首先判断元素是否存在,如果存在,那么我们将元素逻辑上将被访问的元素标记为最新被访问的元素,从实现上,我们可以先将元素从链表中删除,然后重新添加进链表。(cache不用动)
remove操作需要遍历链表,而add很快,因此一旦方法中调用了链表删除操作,那么时间复杂度就是O(n),如果是第一次插入,那么就是O(1)。
cache是缓存,只有当一个元素被换出后才进行会对cache进行移除操作。

面试官随后可能问你,时间复杂度是什么,get()是O(n),而put()如果是第一次插入,且不涉及换出操作则为O(1),否则为O(n)。
这时面试官很有可能让你把get和put方法试着实现成O(1)的,那么对应两种思路

LinkedHashMap扩展

如果看过linkedHashMap源码,可以知道,linkedHashMap的节点继承了hashMap的节点,同时还多了用于维护顺序的两个指针。

    static class Entry<K,V> extends HashMap.Node<K,V> {
   
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
   
            super(hash, key, value, next);
        }
    }

也就是说,linkedHashMap的节点有三个指针,一个是从父节点继承下来的指针,用来解决hash冲突,还有两个指针用来维系某种顺序(插入顺序或者访问顺序)

补充一下,linkedHashMap重写了hashMap的newNode方法,因此可以保证节点数组中存放的都是entry节点而不是node,而且每创建一个节点,都会将这个节点加入链表(不管是插入顺序还是访问顺序,新节点都会加入链表尾部,linkedHashMap中尾部代表最新节点,因此最旧节点是头结点,而访问顺序与插入顺序的最大区别,就是:当一个节点被“访问”后,会做一个后处理,将节点移入链表尾部,而插入顺序是没有这个操作的

linkedHashMap是一个维系了节点顺序的hashMap,底层数组(继承得到)存储节点,而节点通过内部的指针维系某种顺序。

    void af
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值