LRU---基于Java的实现

Least recently used (LRU),中文翻译过来是最近最少使用,是一种容量受限的缓存策略。当新加入一个键时,如果缓存容器已经满了,那么该建将替换最久没有被访问的元素。因此缓存容器内都是最近被访问的元素,这也是符合常理的,一个最近被访问的健,未来一段时间内再次被访问的概率应该是较大的。

LRU缓存的设计在LeetCode上也有对应的题目:https://leetcode.com/problems/lru-cache/description/。一个最基本的LRU缓存应该满足以下一个操作:1、支持指定缓冲器的容量;2、支持put操作(向缓存器中添加元素);3、支持get操作(访问缓存器中的元素)。从调用者的角度来看,调用者的使用可能如下:

LRUCache cache = new LRUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.put(4, 4);    // evicts key 1
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

下面给出两种LRU的具体实现:

方案一:继承LInkedHashMap。

这篇博客中,我们分析了LinkedHashMap的源码,我们已经知道了LInkedHashMap的底层实现原理,其在HashMap的基础上增加了双向链表来维持元素的插入顺序或者访问顺序。并且我们也谈到了,这种设计思路非常的妙,它使双向链表和HashMap共用了key和value。当LinkedHashMap维访问顺序时,每次的get/put操作它都会讲结点移到双向链表的尾部。并且它还提供了一个函数:

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

用来判断是否删除最久没有被访问到的元素,默认的实现是一直放回false,也就是一直不删除最久没被访问到的元素。我们可以重写该方法来实现我们自己的逻辑:比如当容器容量大于某个值时就删除最久没被访问到的元素。

基于这个思路,我们就可以实现第一种方案了:

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

    public LRUCache(int capacity) {
        super(16, 0.75f, true);
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;  // 重写父类方法,当容器容量大于指定大小时就删除最久没被访问的元素
    }
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
}

方案二:使用HashMap+双向链表来实现:

这种方案和上一种是一样的,这两种方案的对比,就更能知道LinkedHashMap中双向链表和hash表共用key,value的区别了。

HashMap保存的是<key,Node>之间的映射,这样知道了key马上就可以找到其对应的结点Node,就可以知道其对应的值了。另外在双向链表的设计上,添加了伪头结点与伪尾结点,避免增加,删除元素时繁琐的判断。还有一点与LinkedHashMap中的区别就是最近访问的元素会被放在双向链表的头部而不是尾部。

class LRUCache {
    static class Node {  // 双向链表的设计,可以看到双向链表里面也保存了一份key,value
        int key;
        int val;
        Node prev;
        Node next;
        public Node() {this(0, 0);}
        public Node(int key, int val) {this.key = key; this.val = val;}
    }
    private int capacity;
    private Node head;
    private Node tail;
    private Map<Integer, Node> map;
    private int size;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.prev = head;
        map = new HashMap<>();
        this.size = 0;
    }
    
    private void addNodeToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }
    
    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private void moveNodeToHead(Node node) {
        removeNode(node);
        addNodeToHead(node);
    }
    
    private Node popTail() {
        Node node = tail.prev;
        removeNode(tail.prev);
        return node;
    }
    
    
    public int get(int key) {
        if(!map.containsKey(key)) {
            return -1;
        }
        Node node = map.get(key);
        moveNodeToHead(node);
        return node.val;
    }
    
    public void put(int key, int value) {
        if(map.containsKey(key)) {
            Node node = map.get(key);
            node.val = value;
            moveNodeToHead(node);
            return ;
        } else {
            size++;
            Node node = new Node(key, value);
            addNodeToHead(node);
            map.put(key, node);
            if(size > this.capacity) {
                Node n = popTail();
                map.remove(n.key);
                size--;
            }
        }
    }
}

代码比较简洁易懂!

最后总结一下:使用方案1,占用的内存空间更小,因为双向链表和hash表共用了同一个key,value。

参考:https://leetcode.com/problems/lru-cache/solution/

猜你喜欢

转载自blog.csdn.net/qq_22158743/article/details/88219288