LRU缓存双向链表O(1)读写,JAVA实现详解

LRU(Least Recently Used) Cache的运行机制,通俗点说,就是缓存最近使用的数据,并淘汰最久没有使用的数据。

LRU的核心思想是数据的时间局部性(Temporal Locality),即"一个被访问的数据,在不久之后很可能还会被再次访问"。把它反过来说就是LRU的实现方式:“最早读取的数据,它不再被使用的可能性比刚刚读取的数据大”。

但是面对周期性读取的数据,如果周期大于缓存容量,那缓存总是无效的。

  • e.g. 假如周期性的读取五个数据:ABCDE,而缓存大小只有3,那么缓存总是不会被命中。

实现LRU本身也是一道面试题。我们就来看看怎么设计和实现吧。

分析与设计

首先来看一下详细的需求,然后设计我们的LRU:

  1. 缓存使用键值对储存,我们使用泛型K来表示键的类型,V来表示值的类型。因此需要实现以下两个接口:
    • public V get(K key)
    • public void put(K key, V value)
  2. 缓存需要设置一个容量。超出容量后会淘汰最长时间没有访问的数据
    • 淘汰最长时间没有访问的数据,也就是先进入缓存的先被删除,我们很自然地就可以联想到队列
    • 当缓存中某个数据被使用时,我们要将这个数据从队列中拿出来放在队列的尾部。而且LRU在每次使用时都要更新使用次序的信息,因此增删数据的时间复杂度对缓存性能至关重要。如此一来,问题就变成了:“用什么数据结构能在最短的时间内增删数元素?”。最短那肯定要从O(1)时间开始思考,而我们正好熟知这样的一种数据结构:链表
    • 同时我们还需要解决怎么尽快从缓存中找到我们的数据。如果仅仅使用链表,我们需要O(n)的时间才能找到想要的元素。而基本数据结构中,能做到O(1)时间根据键查找元素的,就是哈希表了。
    • 结论是:我们使用链表储存实际的数据,来实现缓存队列,然后用哈希表作为数据的索引帮助实现O(1)查找。

下面我们还需要考虑一些细节问题:

  1. 如果想要在O(1)时间内获得尾部节点,或往链表尾部添加节点,我们必须维持一个尾部的引用。
  2. 使用单向链表还是双向链表?都可以,但单向链表有限制。

代码实现(JAVA 1.8)

分析完了,接下来开始准备代码。
首先我们要定义链表的节点,Node<K, V>

  • K:key的类型, V:value的类型
  • 在节点中储存key是为了在删除的时候,能通过节点中储存的key值,在map中使用O(1)时间查找到对应的链表节点

我们选择链表头部作为最新使用的缓存(即队列尾部),链表尾部为最旧的(即队列尾部)。当然,对于双向链表,不管链表头还是尾作队列尾部都可以。根据这个设计,我们需要为链表添加以下几个方法:

void linkFirst(Node<K, V> node)
    添加一个节点到头部
Node<K, V> unlinkLast() {
    删除尾部,并返回删除的节点
void unlink(Node<K, V> node)
    从链表中间任意删除一个节点

添加新的缓存(下面称为newNode)的步骤,每一步都是O(1):

  1. 将newNode加入队列尾部,作为最新的缓存
  2. 在map中添加键值对:newNode.key=newNode
  3. 检查是否超过缓存容量,如果超过,删除最旧的缓存

更新已有的缓存(下面称为node)步骤,每一步也都是O(1):

  1. 根据key值在map中找到node
  2. 将node从队列中移除
  3. 将node加入队列尾部,作为最新的缓存

双向链表的具体操作可以参考LinkedList的源码。
接下来贴出代码:

import java.util.HashMap;

public class LRUCacheDoublyLinkedList<K, V> {
    private int capacity;
    private int size;
    private HashMap<K, Node<K, V>> index;
    /**
     * 最旧值     
     */
    private Node<K, V> first;
    /**
     * 最旧值     
     */
    private Node<K, V> last;

    public LRUCacheDoublyLinkedList(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.first = null;
        this.last = null;
        //设置一个与capacity接近的初始值,减少rehash次数
        index = new HashMap<>(capacity*2);
    }

    public V get(K key) {
        Node<K, V> node = index.get(key);
        if (node == null) {
            return null;
        } else {
            cacheExistingItem(node);
            return node.value;
        }
    }

    public void put(K key, V value) {
        Node<K, V> node = index.get(key);
        //缓存一个新的值
        if (node == null) {
            node = new Node<>(key, value, last, null);
            appendLatest(node);
            if (size > capacity) {
                removeOldest();
            }
        } else {
            node.value = value;
            cacheExistingItem(node);
        }
    }

    private void appendLatest(Node<K, V> node) {
        linkFirst(node);
        index.put(node.key, node);
    }

    private void removeOldest() {
        //删除最旧的数据
        Node<K, V> removedNode = unlinkLast();
        if (removedNode != null) {
            index.remove(removedNode.key);
            // 帮助GC
            removedNode.key = null;
        }
    }

    private void cacheExistingItem(Node<K, V> node) {
        unlink(node);
        linkFirst(node);
    }

    private void linkFirst(Node<K, V> node) {
        if (node == null) {
            return;
        }
        if (first == null) {
            assert last == null;
            last = node;
        } else {
            first.prev = node;
        }
        node.prev = null;
        node.next = first;
        first = node;

        size++;
    }
    private Node<K, V> unlinkLast() {
        if (last == null) {
            return null;
        }
        Node<K, V> copyLast = last;
        last = last.prev;
        if (last == null) {
            first = null;
        } else {
            last.next = null;
        }

        //帮助GC
        copyLast.value = null;
        //不清空copyLast.key,key要用来删除index里面的值
        copyLast.prev = null;

        size--;
        return copyLast;
    }

    private void unlink(Node<K, V> node) {
        if (node == null) {
            return;
        }

        Node<K, V> prev = node.prev;
        Node<K, V> next = node.next;

        if (prev == null) {
            first = next;
        } else {
            assert node != first;
            prev.next = next;
        }
        if (next == null) {
            last = prev;
        } else {
            assert node != last;
            next.prev = prev;
        }

        node.prev = null;
        node.next = null;

        size--;
    }

    @Override
    public String toString() {
        StringBuffer buff = new StringBuffer("LRUCache{" +
                "capacity=" + capacity +
                ", size=" + size +
                ", elements=["
                );
        Node<K, V> node = first;
        while (node != null) {
            buff.append(node.toString());
            buff.append(", ");
            node = node.next;
        }
        buff.append("]}");

        return buff.toString();
    }

    public void showIndexStructure() {
        StringBuffer buff = new StringBuffer('{');
        for (K key : index.keySet()) {
            buff.append("{entry="+index.get(key).toString()+", ");
            buff.append("}, \n");
        }

        System.out.println(buff.toString());
    }

    private static class Node<K, V> {
        /**
         * 用于在O(1)时间更新index
         */
        K key;
        /**
         * 缓存的内容
         */
        V value;

        Node<K, V> prev;
        Node<K, V> next;

        public Node(K key, V value, Node<K, V> prev, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.prev = prev;
            this.next = next;
        }

        @Override
        public String toString() {
            return "Node{"+ key +
                    ": " + value +
                    '}';
        }
    }

}

测试类:

import org.junit.Assert;
import org.junit.Test;

public class TestLRUCache {
    @Test
    public void testDoublyLinkedLRUCache() {
        LRUCacheDoublyLinkedList<Integer, Integer> cache = new LRUCacheDoublyLinkedList<>(3);
        System.out.println(cache);
        Assert.assertEquals(null, cache.get(5));
        cache.put(5, 55);
        cache.put(5, 55);
        System.out.println(cache);
        cache.put(4, 44);
        System.out.println(cache);
        cache.put(10, 0);
        System.out.println(cache);
        cache.put(2, 22);
        System.out.println(cache);
        cache.showIndexStructure();

        Assert.assertEquals(Integer.valueOf(44), cache.get(4));
        System.out.println(cache);
        cache.showIndexStructure();
    }
}

码字不易,觉得有帮助就给我点个赞吧!我会继续努力的!

Reference:
局部性原理,百度百科
LRU,百度百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值