手撕LRU缓存【基于双向链表和哈希表】【基于重写LinkedHashMap】(JAVA实现)

转载本文章请标明作者和出处
本文出自《爱喝纯净水的南荣牧歌》
本文题目和部分思路来源自leetcode

在这里插入图片描述

加油,程序猿!!!

缘起

正好最近使用谷歌的LoadindCache做一个本地堆缓存,不仅对这个谷歌的高性能工具,产生了兴趣,于是上网查了些许资料,了解到LoadindCache使用了LRU淘汰算法,于是顺手查了一下相关资料;

什么是LRU算法

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

  • 图示
    假如我们有一块缓存,里面缓存了1、2、3三个数据;
    手撕LRU缓存【基于双向链表和哈希表】【基于重写LinkedHashMap】(JAVA实现)_第1张图片
    这时,用户请求数据2,那么数据2将会被移动最后面;
    手撕LRU缓存【基于双向链表和哈希表】【基于重写LinkedHashMap】(JAVA实现)_第2张图片
    如果这是新加了数据4,那么数据4直接就会被放到最下面
    手撕LRU缓存【基于双向链表和哈希表】【基于重写LinkedHashMap】(JAVA实现)_第3张图片
    如果这时缓存只维护两个数据的话,最上面的两个数据3、数据1就会被淘汰掉;
    手撕LRU缓存【基于双向链表和哈希表】【基于重写LinkedHashMap】(JAVA实现)_第4张图片

手撸实现

正好,在leetcode上有一道手写LRU缓存的题目,一时兴起,敲到现在,看看现在的时间,脑袋壳儿疼;

题目

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

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

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

  • 示例

    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
    

思路

  • 双向链表和HashMap

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

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

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


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

实现代码

代码环境为JDK1.8

  • 双向链表和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;
        }
    }
    
    • 重写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);
        }
    }
    

    太晚了,,,,睡觉了,;

喜欢的朋友可以加我的个人微信,我们一起进步

你可能感兴趣的:(缓存,java,缓存)