LRU缓存

leetcode题目-16.25.LRU缓存

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/lru-cache-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

即如果一组数字,最近使用的放在最左边,最近不用的放在最右边。因此如果新写入一个数字,如果内存满了,就把最右边的数字替换掉,新来的数字放在最左边。如果新获取一个数据,那么这个数据就是最新使用的了,就更新它的位置。

因此这组数据需要频繁地换位置,肯定是要使用链表的。

//LinkedHashMap实现
class
LRUCache { int capacity; Map<Integer, Integer> map; public LRUCache(int capacity) { this.capacity = capacity; map = new LinkedHashMap<>(); } public int get(int key) { //若密钥不存在缓存中,则返回-1 if(!map.containsKey(key)){ return -1; } //如果密钥存在缓存中,则获取密钥的值 Integer value = map.remove(key); map.put(key, value); return value; } public void put(int key, int value) { //如果密钥存在,删出原数值.即更新该数值的位置 if(map.containsKey(key)){ map.remove(key); map.put(key, value); return; } //如果密钥不存在,写入其数据值 map.put(key, value); //如果缓存容量达到上限,那么删出最近最少使用的数据 //利用迭代器,删出第一个 if(map.size()>capacity){
//map.entrySet():把HashMap类型的数据转换为集合类型,获取键值对的集合
       //iterator():获取这个集合的迭代器 map.remove(map.entrySet().iterator().next().getKey()); } } }
//双向链表+HashMap
public
class LRUCache{ //定义双向链表节点 private class ListNode{ int key; int value; ListNode pre; ListNode next; public ListNode(int key, int value){ this.key = key; this.value = value; pre = null; next = null; } } private int capacity; private Map<Integer, ListNode> map; //虚拟头节点 private ListNode head; //虚拟尾节点 private ListNode tail; //初始化 public LRUCache(int capacity){ this.capacity = capacity; map = new HashMap<>(); head = new ListNode(-1, -1); tail = new ListNode(-1, -1); //建立虚拟头节点和尾节点的关系 head.next = tail; tail.pre = head; } public int get(int key){ //若密钥不存在缓存中,则返回-1 if(!map.containsKey(key)){ return -1; } //如果密钥存在缓存中,则获取密钥的值 ListNode node = map.get(key); //更新密钥对应值的位置到尾部.
//现在原位置删除当前节点
node.pre.next = node.next; node.next.pre = node.pre;
//把当前节点加在尾部 moveToTail(node);
return node.value; } public void put(int key, int value){ //如果密钥存在,删出原数值.即更新该数值的位置 if(get(key) != -1){ map.get(key).value = value; return; } //如果密钥不存在,写入其数据值 ListNode node = new ListNode(key, value); map.put(key, node); moveToTail(node); //如果缓存容量达到上限,那么删出最近最少使用的数据。删除头(虚拟头节点后面的节点) if(map.size() > capacity){ map.remove(head.next.key); head.next = head.next.next; head.next.pre = head; } } //将节点移动到链表的末尾,虚拟尾节点的前面 private void moveToTail(ListNode node){ node.pre = tail.pre; tail.pre = node; node.pre.next = node; node.next = tail; } }

 

带过期时间功能:

添加一个过期时间队列&一个过期清除线程,清除的时候使用while(true)判断队列队首位置是否过期

为每个节点放一个过期时间,只要到了这个时间就直接删除。只要启动LRU,就开始清除

public class LRU{
      //设置清除过期数据的线程池
      private static ScheduleExecutorService swapExpiredPool = new ScheduledThreadPoolExecutor(10);
      //用户存储数据,ConcurrentHashMap用于保证线程安全
private ConcurrentHashMap<String, Node> cache = new ConcurrentHashMap<>(1024);
//保存最新的过期数据,过期时间最小的排在队列前 private PriorityQueue<Node> expireQueue = new PriorityQueue<>(1024); //构造方法。只要启动了这个LRU,过期清除线程就开始工作 public LRU(){ swapExpiredPool.scheduleWithFixedDelay(new ExpiredNode(), 3, 3,TimeUnit.SECONDS); } //....... }

ExpireNode当做一个内部类在LRU中

public class ExpiredNode implements Runnable{
       public void run(){
              //获取当前时间
              long now = System.currentTimeMillis();
              while(true){
                     //从过期队列弹出队首元素
                     Node node = expireQueue.peek();
                     //如果不存在或者不过期,就返回
                     if(node==null || node.expireTime>now)
                              return;
                     //如果过期,就从队列里弹出
                     cache.remove(node.key);
                     expireQueue.poll();   
              }
       }  
}                                                                    

 那么相应的set方法也要有改变,因为要考虑过期时间。Node节点里多一个ExpireTime的字段

    public void put(int key, int value, long ttl){
        //如果密钥存在,删出原数值.即更新该数值的位置
        if(get(key) != -1){
            map.get(key).value = value;
            return;
        }
        //获取过期时间点
        long expireTime = System.currentTimeMillis()+ttl;
        //如果密钥不存在,写入其数据值
        ListNode node = new ListNode(key, value, expireTime);
//cache中有的话就覆盖,没有的话添加新的。&过期时间队列也要添加 ListNode old
= cache.put(key, node); expireQueue.add(node); //如果该key存在数据,要从过期时间队列里删除 if(old!=null){ expireQueue.remove(old); return old.value; } return null; }

 

 

扩展:

LRU应用场景:

日常开发中,UI界面加载图片不可能每次都从网络上下载然后显示,因此Android提供了LruCache类,用于图片的内存缓存

A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
一个包含有限数量强引用(平常使用的对象引用方式)的缓存,每次访问一个值,它都会被移动到队列的头部,将一个新的值添加到已经满了的缓存队列时,该队列末尾的值将会被逐出,并且可能会被垃圾回收机制进行回收。

内部实现是通过LinkedHashMap维护一个缓存对象列表。参数分别为初始容量、加载因子、访问顺序(为true即集合的元素顺序是访问顺序,访问后会将该元素放到集合的最后面;为false即按照插入顺序)。

初始容量的设置:如初始大小小于1,那么map大小默认为1;否则不断*2直到大于设置的初始容量。

总缓存大小一般为可用内存的1/8

另一种采用LRU算法的缓存为DisLruCahce,用于实现硬盘缓存。

Java的集合:

  • Collection接口
    • set接口(集):唯一,无序。实现类都线程不安全
      • HashSet:底层是一个数组,适用于少量数据的插入操作
        • LinkedHashSet:继承了HashSet类,为了保持数据的先后添加顺序,又加了链表结构,但是效率低。若某个集合需要保证元素不重复&记录元素的添加顺序
      • TreeSet:也实现了SortSet接口,底层红黑树,只能存储相同类型对象的引用
    • list接口(列表):可重复,顺序与插入顺序一致
      • ArrayList:底层为数组结构,查询快,增删改慢
      • Vector:数组。比于ArrayList,由于每个方法都加上了synchronized,因此线程安全&效率低于ArrayList。由于增长率是目前数组长度的100%,ArrayList为50%,因此Vector适合存储数据量比较大的数据。
      • LinkedList:底层为链表结构,查询慢,增删改快
  • Map接口:键唯一,值不一定唯一
    • HashMap:无序
      • LinkedHashMap:HashMap+LinkedList。对读取顺序有严格要求时使用,继承HashMap,实现了Map接口。桶的链表是双向链表,并且可以控制存储顺序。“HashMap桶的链表产生是因为产生hash碰撞,所有数据形成链表 (红黑树) 存储在一个桶中,LinkedHashMap 中双向链表会串联所有的数据,也就是说有桶中的数据都是会被这个双向链表管理。”即桶里的链表也要实现双向链表的功能(图源于:https://www.cnblogs.com/xiaoxi/p/6170590.html)
    • TreeMap:基于红黑树实现,根据键的自然顺序进行排序
    • HashTable:无序,任何非空的对象都可作为key/value,线程安全

迭代器iterator

Java采用迭代器为各种容器提供公共的操作接口,使得对容器的遍历操作与具体的底层实现相隔离。

“Collection集合元素的通用获取方式:在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。”

因此迭代器要实现两个方法:

hasNext():仍有元素可迭代,返回true;

next():返回迭代的下一个元素。

 

posted @ 2020-07-26 23:31  闲不住的小李  阅读(1140)  评论(0编辑  收藏  举报