手把手带你实现一个LRU缓存

123 阅读3分钟

文章开头,送一句我比较信奉的话给大家

image.png

什么是LRU算法

LRU是什么?按照英文的直接原义就是Least Recently Used,最近最久未使用法,它是按照一个非常著名的计算机操作系统基础理论得来的:最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间,附加指标是使用的次数。在计算机中大量使用了这个机制,它的合理性在于优先筛选热点数据,所谓热点数据,就是最近最多使用的数据!

image.png

示例

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
  
  cache.put(1, 1);
  cache.put(2, 2);
  cache.get(1);       // 返回  1
  cache.put(3, 3);    // 该操作会使得密钥 2 作废
  cache.get(2);       // 返回 -1 (未找到)
  cache.put(4, 4);    // 该操作会使得密钥 1 作废
  cache.get(1);       // 返回 -1 (未找到)
  cache.get(3);       // 返回  3
  cache.get(4);       // 返回  4

思路

1)双向链表+HashMap

我们可以定义一个双向链表,用来存储需要缓存的数据,并声明一个假的头和尾节点,可以省去很多判空的操作(也是双向链表问题的惯用手段);然后再使用一个HashMap(哈希表)来快速定位节点的位置,并且还可以快速的知道缓存中有多少数据,以判断是否超过容量值(否则以双向链表的特性,我们就要遍历了);

当新增一个元素的时候,我们先判断哈希表中是否存在这个key,如果有的话,我们只需要修改这个value的值,并把这个节点移动到链表的最后,如果哈希表中没有的话,我们需要往哈希表中放一个,并且新建一个节点放到链表的末尾,还要判断此时哈希表的容量是否超过缓存的最大容量,超过的话,就需要淘汰掉链表的头元素,直到等于缓存的容量值;

获取元素的时候我们只需要从哈希表中获取,并把这个节点移动到链表最后,并返回值即可;

image.png

2)重写LinkedHashMap LinkedHashMap构造函数里面有一个参数accessOrder,如果指定这个参数为true的话,每次get的Entry会被放到链表的最后面; 还有一个boolean removeEldestEntry;(Map.Entry eldest)方法,用来判断什么时候移除链表的起始元素; 通过这两个特性即可实现LRU,但是我建议不要只会这一种,否则我们就是api调用程序员了;

手撸实现

1)双向链表和HashMap

 class LRUCache {
  
      // 定义一个双向链表
      static class Node {
          Integer key;
          Integer value;
  
          public Node(Integer key, Integer value) {
              this.key = key;
              this.value = value;
          }
  
          Node pre;
          Node next;
      }
  
      // 用来快速定位节点和记录节点数量
      private HashMap<Integer, Node> map;
      // 虚拟头节点
      private Node dummyFirst;
      // 虚拟尾节点
      private Node dummyLast;
      // LRU的容量
      private int capacity;
  
      /**
       * 初始化方法
       * @param capacity 指定缓存的容量
       */
      public LRUCache(int capacity) {
          map = new HashMap<>(capacity);
          dummyFirst = new Node(-1, -1);
          dummyLast = new Node(-1, -1);
          // 建立虚拟头和虚拟尾节点的关系
          dummyFirst.next = dummyLast;
          dummyLast.pre = dummyFirst;
          this.capacity = capacity;
      }
  
      /**
       * 从缓存中获取数据
       * @param key 缓存的键
       * @return 缓存的值
       */
      public int get(int key) {
          // 如果map中没有这个key,证明没有命中缓存,直接返回-1即可
          if (!map.containsKey(key)) {
              return -1;
          }
          Node target = map.get(key);
          // 将命中缓存的节点移到链表的最末尾(虚拟尾节点前面)
          moveToTail(target, false);
          return target.value;
      }
  
      /**
       * 向缓存中写入数据
       * @param key 写入的键
       * @param value 写入的值
       */
      public void put(int key, int value) {
          // 如果这个map存在的话,只需要把这个节点移到链表的最末尾(虚拟尾节点前面),并修改链表的值即可
          if (map.containsKey(key)) {
              moveToTail(map.get(key), false);
              map.get(key).value = value;
              return;
          }
          // 如果map不存在的话,需要在map和链表的最末尾(虚拟尾节点前面)新增这个节点,并且检查现在缓存超没超容量,如果超了的话需要删除链表的最前面的节点(虚拟头节点的后面)
          Node node = new Node(key, value);
          map.put(key, node);
          moveToTail(node, true);
          while (map.size() > capacity) {
              map.remove(dummyFirst.next.key);
              dummyFirst.next = dummyFirst.next.next;
              dummyFirst.next.pre = dummyFirst;
          }
      }
  
      /**
       * 将节点移动至链表的末尾,假末尾节点前面
       */
      private void moveToTail(Node node, boolean insert) {
          // 如果不是新增,而是修改,我们要维护原节点的pre和next节点的next和pre引用
          if (!insert) {
              node.pre.next = node.next;
              node.next.pre = node.pre;
          }
          // 将节点移动到链表的最末尾(虚拟尾节点前面)
          node.next = dummyLast;
          node.pre = dummyLast.pre;
          dummyLast.pre = node;
          node.pre.next = node;
      }
  }

2)重写LinkedHashMap

class LRUCache {
  
      private int capacity;
      private LRUList<Integer, Integer> list;
  
      class LRUList<K, V> extends LinkedHashMap<K, V> {
  
          public LRUList(int capacity) {
              super(capacity, 0.75F, true);
          }
  
          @Override
          protected boolean removeEldestEntry(Map.Entry eldest) {
              return list.size() > capacity;
          }
      }
  
      public LRUCache(int capacity) {
          this.capacity = capacity;
          this.list = new LRUList(capacity);
      }
  
      public int get(int key) {
          return list.getOrDefault(key, -1);
      }
  
      public void put(int key, int value) {
          list.put(key, value);
      }
  }

喜欢的话,欢迎关注并转发公众号,你们的支持是我前进的动力!

公众号.jpg