0002 LRU算法浅析

Posted on Thu, Oct 14, 2021 算法 JAVA

✏️原理概述:LRU(Least Recently Used),最近最少使用原则实现的缓存淘汰算法。

“最近最少”的误解:很多人看字面意思很可能理解为“最近使用和最少使用的被淘汰”,理解几乎完全相反了。

✔️“最近最少”的正确理解:基于这一种准则:刚使用过的、最新添加的元素,被(再次)使用到的概率更大,因此要放到队列读取的最前端,最先淘汰队列尾端的元素(说明它被添加的时间很长了而且很少被访问到过,若是新添加的、经常被访问过的应该在靠近读取端头部)

看这样一个例子:面试题 16.25. LRU 缓存

//简单删了几个方法
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.put(4, 4);    // 该操作会使得密钥 1 作废

✏️下面是图解:

💠实现方式① :利用LinkedHashMap的accessOrder

class LRUCache {
    private int capacity;
    private Map<Integer, Integer> map;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new LinkedHashMap(capacity);  //单纯使用LinkedHashMap的插入顺序与输出顺序一致

    }

    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        int val = map.get(key);
        //重新put,将访问过的元素提到队头(访问方向)
        map.remove(key);
        map.put(key, val);

        return val;

    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            map.remove(key);
            map.put(key, value);
            return;
        }
       //超过容器大小,删除队尾。
       //刚好LinkedHashMap的迭代指针默认指向队尾,所以直接删除指针第一个
        if (map.size() >= capacity)
            map.remove(map.keySet().iterator().next());

        map.put(key, value);

    }
}

💠实现方式②:继承LinkedHashMap重写

LinkedHashMap的最大价值就是缓存淘汰实现,其内部已经被实现了LRU基础功能,需要修改的是removeEldestEntry方法,即在什么情况下才会触发删除策略,我们这里是当容器满了的时候,触发删除策略

class LRUCache extends LinkedHashMap<Integer, Integer>{
    private int capacity;
    //增加负载因子,超过容量会自动删除
    private static final Float CAPACITY_FACTOR = 0.75f;

    public LRUCache(int capacity) {
        super(capacity, CAPACITY_FACTOR, 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);
    }
    //重写removeEldestEntry方法,当超过容器元素时移除最“老”元素
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        //size()为 父类已有的方法
        return size() > capacity;

    }
}

💠实现方式③:HashMap + 双向链表

操作方式:读取在链表尾,删除在链表头,所以要增加移动到链表尾的操作。

注意,双向链表中,head和tail是哨兵节点,不存储实际值,实际链表的值在两者之间。

public class LRUCache{

    private int capacity;
    private Map<Integer, ListNode> map; //key->node
    private ListNode head;  // dummy head
    private ListNode tail;  // dummy 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) {
        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.val;
    }

    public void put(int key, int value) {
        // 直接调用这边的get方法,如果存在,它会在get内部被移动到尾巴,不用再移动一遍,直接修改值即可
        if (get(key) != -1) {
            map.get(key).val = value;
            return;
        }
        // 若不存在,new一个出来,如果超出容量,把头去掉
        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;
    }

    // 定义双向链表节点
    private class ListNode {
        int key;
        int val;
        ListNode pre;
        ListNode next;

        public ListNode(int key, int val) {
            this.key = key;
            this.val = val;
            pre = null;
            next = null;
        }
    }
}

💠实现方式④:HashMap + 单向链表

public class LRUCache{

    private int capacity;
    private Map<Integer, ListNode> map; //key -> node.pre
    private ListNode head;  // dummy
    private ListNode tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        head = new ListNode(-1, -1);
        tail = head;
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        // map中存放的是要找的节点的前驱
        ListNode pre = map.get(key);
        ListNode cur = pre.next;

        // 把当前节点删掉并移到尾部
        if (cur != tail) {
            pre.next = cur.next;
            // 更新它后面 node 的前驱
            map.put(cur.next.key, pre);
            map.put(cur.key, tail);
            moveToTail(cur);
        }
        return cur.val;
    }

    public void put(int key, int value) {
        if (get(key) != -1) {
            map.get(key).next.val = value;
            return;
        }
        // 若不存在则 new 一个
        ListNode node = new ListNode(key, value);
        // 当前 node 的 pre 是 tail
        map.put(key, tail);
        moveToTail(node);

        if (map.size() > capacity) {
            map.remove(head.next.key);
            map.put(head.next.next.key, head);
            head.next = head.next.next;
        }
    }

    private void moveToTail(ListNode node) {
        node.next = null;
        tail.next = node;
        tail = tail.next;
    }

    // 定义单链表节点
    private class ListNode {
        int key, val;
        ListNode next;

        public ListNode(int key, int val) {
            this.key = key;
            this.val = val;
            this.next = null;
        }
    }

}