如何设计实现一个LRU Cache

这道题是把操作系统和Java集合类联系起来的一道题目,是非常值得研究的一道题目。(思考点:缓存的算法设计和需要缓存的数据是以什么样的数据格式存放在那种数据结构中,这种数据结构可以有效的实现缓存的作用。)

146 LRU缓存机制

题目

运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
进阶:         
你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例

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

最近最久未使用置换算法(LRU)

选择最近最长时间未被访问过的页面予以淘汰,它认为过去一段内未访问过的页面,在最近的将来可能也不会被访问。该算法为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大(就是最长时间)的予以淘汰。

如何设计实现一个LRU Cache_第1张图片
1.jpg

详细过程可以参考下面的:

如何设计实现一个LRU Cache_第2张图片
image.png

LRU缓存机制

了解了什么是LRU最近最久未使用置换算法后。我们在来思考一下如何去设计这个缓存系统,利用什么样的数据结构区存储这些数据,而这样的数据结构该符合什么样的特点呢?

基于HashMap和双向链表实现LRU

参考:[不懂机器学习的架构师不是好CTO]

整体的设计思路是,可以使用HashMap存储Key,这样可以做到save(key,value)get(key)的时间是O(1),而HashMap的Value指向双向链表实现的LRU的Node节点,如下图所示:

如何设计实现一个LRU Cache_第3张图片
image.png
如何设计实现一个LRU Cache_第4张图片
image.png

LRU存储是基于双向链表实现的,下图演示了它的原理。其中head代表了双向链表的表头,tail代表了尾部。首先预先设置了LRU的容量,如果存储满了,可以通过O(1)的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过O(1)的效率把新的节点增加到头部,或者把已经存在的节点移动到头部。

下面预设HashMap的容量为3,下面展示了LRU缓存在存储和访问过程中的变化。为了简化图的复杂度,图中没有展示HashMap部分的变化,仅仅演示了上图LRU双向链表的变化。我们对这个LRU缓存的操作序列如下:

save("key1", 7)
save("key2", 0)
save("key3", 1)
save("key4", 2)
get("key2")
save("key5", 3)
get("key2")
save("key6", 4)

相应的 LRU 双向链表部分变化如下:

如何设计实现一个LRU Cache_第5张图片
image.png

总结一下核心操作的步骤:

  • 1.save(key, value),首先在 HashMap找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。

  • 2.get(key),通过HashMap找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。

参考下面视频中的代码:cspiration

//初始化双向链表的节点
class DLinkedNode {
    int key;
    int value;
    DLinkedNode pre;
    DLinkedNode post;
}

/**
 * Always add the new node right after head;
 */
private void addNode(DLinkedNode node){
    node.pre = head;
    node.post = head.post;
    
    head.post.pre = node;
    head.post = node;
}

/**
 * Remove an existing node from the linked list.
 */
private void removeNode(DLinkedNode node){
    DLinkedNode pre = node.pre;
    DLinkedNode post = node.post;
    
    pre.post = post;
    post.pre = pre;
}

/**
 * Move certain node in between to the head.
 */
private void moveToHead(DLinkedNode node){
    this.removeNode(node);
    this.addNode(node);
}

// pop the current tail. 
private DLinkedNode popTail(){
    DLinkedNode res = tail.pre;
    this.removeNode(res);
    return res;
}

    private Hashtable 
    cache = new Hashtable();
    private int count;
    private int capacity;
    private DLinkedNode head, tail;

public LRUCache(int capacity) {
    this.count = 0;
    this.capacity = capacity;

    head = new DLinkedNode();
    head.pre = null;
    
    tail = new DLinkedNode();
    tail.post = null;
    
    head.post = tail;
    tail.pre = head;
}

public int get(int key) {
    
    DLinkedNode node = cache.get(key);
    if(node == null){
        return -1; // should raise exception here.
    }
    // move the accessed node to the head;
    this.moveToHead(node);
    return node.value;
}


public void set(int key, int value) {
    DLinkedNode node = cache.get(key);
    //判断这个节点是否为null
    if(node == null){
        DLinkedNode newNode = new DLinkedNode();
        newNode.key = key;
        newNode.value = value;
        this.cache.put(key, newNode);
        this.addNode(newNode);
        ++count;
        if(count > capacity){
            // pop the tail
            DLinkedNode tail = this.popTail();
            this.cache.remove(tail.key);
            --count;
        }
    }else{
        // update the value.
        node.value = value;
        this.moveToHead(node);
    }
    
}

你可能感兴趣的:(如何设计实现一个LRU Cache)