LRU Cache、LinkedHashMap源码分析2.0

一、LRU Cache

LRU Cache(最近最少使用缓存机制):当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间

LRU Cache工作示例:

在这里插入图片描述

LRU Cache一般使用哈希表+双向链表来实现,支持时间复杂度为 O ( 1 ) O(1) 的查询、修改、更新操作

二、LinkedHashMap源码分析2.0

LinkedHashMap源码分析1.0:https://blog.csdn.net/qq_40378034/article/details/102730778

LinkedHashMap支持两种访问访问顺序,这主要取决于accessOrder这个参数的值,当accessOrder为false时按照插入顺序访问(默认),当accessOrder为true时按照LRU Cache的机制进行访问

    //initialCapacity:初始化容量 loadFactor:负载因子 accessOrder:访问顺序(true代表使用LRU/false代表使用插入的顺序)
	public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

当某个位置的数据被命中,通过调整该数据的位置,将其移动至尾部。新插入的元素也是直接放入尾部(尾插法)。这样一来,最近被命中的元素就向尾部移动,那么链表的头部就是最近最少使用的元素所在的位置

LinkedHashMap中并没有覆写任何关于HashMap的put方法,所以调用LinkedHashMap的put方法实际上调用了父类HashMap的方法

HashMap中put方法源码如下:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断当前桶是否为空,空的就需要初始化(resize中会判断是否需要初始化)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据当前key的hashcode定位到具体的桶中并判断是否为空,为空表明没有Hash冲突就直接在当前位置创建一个新桶即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,后面统一进行赋值及返回
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果当前桶为红黑树,按照红黑树的方式写入数据
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //如果是个链表,就需要将当前的key、value封装成一个新节点写入当前桶的后面(采用尾插法)
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果在遍历链表的过程中,找到key相同时直接退出遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果e!=null就相当于存在相同的key,那就需要将值覆盖
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //最后判断是否需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在putVal方法中如果map中存在相同的key时,会调用void afterNodeAccess(Node<K,V> p)方法,该方法在HashMap中是空实现,但是在LinkedHasMap中重写了该方法实现了将被访问节点移动到链表最后

    //将被访问节点移动到链表最后
	void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //accessOrder为true时才支持LRU Cache
        if (accessOrder && (last = tail) != e) {
            //三个临时变量:p为当前被访问节点,b为其前驱结点,a为其后继节点
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //访问节点的后驱节点置为null
            p.after = null;
            //如果访问节点的前驱为null,则说明p=head,由于这时p要移动到链表最后,所以a设置为head
            if (b == null)
                head = a;
            //否则b的后继设置为a
            else
                b.after = a;
            
            //如果p不为尾节点,那么将a的前驱设置为b   
            if (a != null)
                a.before = b;
            else
                last = b;
            
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            //将p接在双向链表的最后
            tail = p;
            ++modCount;
        }
    }

举个例子,比如该次操作访问的是13这个节点,而14是其后驱,11是其前驱,且tail=14。在通过get访问13节点后,13变成了tail节点,而14变成了其前驱节点,相应的14的前驱变成11,11的后驱变成了14,14的后驱变成了13

扫描二维码关注公众号,回复: 8875946 查看本文章

在这里插入图片描述

而在putVal方法的最后会调用一个void afterNodeInsertion(boolean evict)方法,,该方法在HashMap中是空实现,但是在LinkedHasMap中重写了该方法实现了删除头节点(最近最少使用的元素)

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {//(1)
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

代码(1)处:evict在put方法调用putVal时传参即为true,所以当map不为空且removeEldestEntry返回true时就会删除头节点,但是在LinkedHasMap中removeEldestEntry方法始终返回true,所以如果要基于LinkedHashMap实现LRU则需要重写removeEldestEntry方法,当map的size大于初始化容量时返回true

参考:https://juejin.im/post/5ace2bde6fb9a028e25deca8

三、LeetCode146:LRU缓存机制

运用你所掌握的数据结构,设计和实现一个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

题解1:

public class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;

    public LRUCache(int capacity) {
        //initialCapacity:初始化容量 loadFactor:负载因子 accessOrder:访问顺序(true代表使用LRU/false代表使用插入的顺序)
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

题解2:

解题思路:哈希表+双向链表(头尾虚节点)

public class LRUCache {
    //key -> Node(key, val)
    private HashMap<Integer, Node> map;
    //Node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;
    //最大容量
    private int cap;

    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }

    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        int val = map.get(key).val;
        //利用put方法把该数据提前
        put(key, val);
        return val;
    }

    public void put(int key, int val) {
        //新节点node
        Node node = new Node(key, val);

        if (map.containsKey(key)) {
            //删除旧的节点,新的插到头部
            cache.remove(map.get(key));
            cache.addFirst(node);
            //更新map中对应的数据
            map.put(key, node);
        } else {
            if (cap == cache.size()) {
                //删除链表最后一个数据
                Node last = cache.removeLast();
                map.remove(last.key);
            }
            //直接添加到头部
            cache.addFirst(node);
            map.put(key, node);
        }
    }

    class Node {
        public int key, val;
        public Node prev, next;

        public Node(int k, int v) {
            this.key = k;
            this.val = v;
        }
    }

    class DoubleList {
        private Node head, tail; //头尾虚节点
        private int size; //链表元素数

        public DoubleList() {
            head = new Node(0, 0);
            tail = new Node(0, 0);
            head.next = tail;
            tail.prev = head;
            size = 0;
        }

        //在链表头部添加节点node
        public void addFirst(Node node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
            size++;
        }

        //删除链表中的node节点(node一定存在)
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
            size--;
        }

        //删除链表中最后一个节点,并返回该节点
        public Node removeLast() {
            if (tail.prev == head)
                return null;
            Node last = tail.prev;
            remove(last);
            return last;
        }

        //返回链表长度
        public int size() {
            return size;
        }
    }
}

常用数据结构的时间、空间复杂度:
在这里插入图片描述
https://www.bigocheatsheet.com/

发布了177 篇原创文章 · 获赞 407 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/103335470