面试常问的LRU缓存模式,即LRUCache

定义就不复制粘贴了。这道题目之所以有区分度,是因为考察了面试者对数据结构的应用,而不是什么计算机底层的“缓存”知识。
我们的任务,就是在有限的空间中存储<K, V>形式的数据。如果“进来”的数据太多以至于存不下,那么就删掉最不常被访问的数据。所以,需要记录数据本身,以及被访问的顺序。如果这么简单的话,实现的方法很多。但问题的关键在于:
既然是高速缓存,则需要在固定的空间内,满足增删改查O(1)。
熟悉数据结构的小伙伴都知道,没有一种数据结构满足这样变态的要求。那么,答案是什么呢?
我在快手二面的时候,就被难住了。看到这里,小伙伴不妨自己思考一下,怎么解决这个问题。

好吧,不卖关子了。
既然一种数据结构无法满足,那就两个!
答案就是,HashMap + 双链表。
在这里插入图片描述
感谢这张图的作者。如果没有这张图,我恐怕还要半个月才能彻底理解LRU的核心思想:-)
废话不多说,直接上代码。

package interview;

import java.util.HashMap;

public class LRUCache {
    /*
     * Least Recent Used
     * 淘汰掉最不经常使用的
     * 使用HashMap + 双链表,实现CRUD时间复杂度为O(1)
     */

    /*
     * 使用内部类定义一个双链表
     * 至于为什么是这四个成员变量呢?
     * LRU最大的特点,就是一个双链表的节点,同时也是HashMap的value
     * 那么HashMap的key从哪里来呢?
     * 没错,HashMap中的key和双链表节点中的key是同一个东西。
     * 这样说起来好像很奇怪,很啰嗦,很不优雅。然而最好,甚至必须这样做。解释如下:
     * 想象两个情形:
     * 1. 只在双链表中存value。那么——
     * 我们要弹出双链表最后一个元素,需要在双链表和HashMap中进行两次删除操作。
     * 但在HashMap中的删除操作,要借助双链表中的信息。
     * 废话,因为只有双链表知道谁是“最不常使用的元素”啊。
     * 而如果双链表中没有key,那岂不是没法在HashMap中以O(1)时间找到这个元素。
     * 2. 只在双链表中存key。那么——
     * 请问你value打算放哪里?你仿佛在搞笑哦。= =
     */
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode pre;
        DLinkedNode next;
    }

    /*
     * 这里是成员变量,private可以去掉,没什么影响
     * 重要的事情再说一遍:
     * LRU最大的特点,就是一个双链表的节点,同时也是HashMap的value
     * cnt is short for count,就是目前的元素数量,作为判断。
     * cap is short for capacity,就是最大容量
     * hh和tt,是head和tail的卖萌版。虚拟头结点和尾节点。
     * 此时,他们是LRU数据结构的成员变量。
     */
    private HashMap<Integer, DLinkedNode> h = new HashMap<>();
    private int cnt;
    private int cap;
    private DLinkedNode hh, tt;

    /*
     * 初始化,没什么好讲的。
     */
    public LRUCache(int cap) {
        this.cnt = 0;
        this.cap = cap;

        hh = new DLinkedNode();
        tt = new DLinkedNode();
        hh.pre = null;
        tt.next = null;

        hh.next = tt;
        tt.pre = hh;
    }

    /*
     * 在某一元素被访问到的时候,先调用这个toHead()方法.
     * 这样,链表中从前到后的顺序,刚好就是被访问到的顺序。
     * 很幸运,对链表当中顺序的改动,并不影响HashMap。
     * 因为,改变链表的顺序,只需要改变它的前后指向,
     * 而不需要变动它自身的内存地址。
     * 何况,链表本身在内存中的存储也并不是一块连续的空间。
     * 这个方法本身的实现很简单:
     * 先让这个节点跟它的前后节点断开,然后头插。
     */
    private void toHead(DLinkedNode node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
        this.push(node);
    }

    /*
     * 头插,不解释~
     */
    private void push(DLinkedNode node) {
        node.pre = this.hh;
        node.next = this.hh.next;
        this.hh.next.pre = node;
        this.hh.next = node;
    }

    /*
     * 很不幸,优先级最低的节点要说再见了。
     * 那篇点击量最高的文章,拜托,你都没有维护链表诶!
     * 虽然你那样也可以运行。哈哈~
     */
    private void popTail() {
        DLinkedNode tmp = tt.pre;
        tmp.pre.next = tt;
        tt.pre = tmp.pre;
        h.remove(tmp.key);
        tmp = null;
    }

    /*
     * 通过Key获取元素,同时把这个被访问的元素设为表头的元素。
     * 刚刚被访问,优先级自然是最高。
     * 如果元素不存在,返回-1。
     */
    public int get(int key) {
        DLinkedNode tmp = h.get(key);
        if (tmp == null) return -1;

        this.toHead(tmp);

        return tmp.value;
    }

    /*
     * 这里就是最重要的set()方法,即“创建或修改”操作。
     * 一般的题目,就是要求set()和get()方法。
     * 也只有这个方法中,包含了缓存的容量这一参数。
     * 虽然前面看似是铺垫,但个人认为,前面的操作才是核心思想。
     * 前面的弄懂了,这里自然懂。
     * 实现起来也没难度,只要理顺逻辑就好。
     * 千万别把else写到里面那层代码里去。
     * 更不要自作聪明,把对cnt的操作放在前面的函数里面,把自己绕进去了。
     */
    public void set(int key, int value) {
        DLinkedNode tmp = h.get(key);
        if (tmp == null) {
            tmp = new DLinkedNode();
            tmp.key = key;
            tmp.value = value;

            this.h.put(key, tmp);
            this.push(tmp);

            cnt ++ ;

            if (cnt > cap) {
                this.popTail();
                cnt -- ;
            }

        } else {
            tmp.value = value;
            this.toHead(tmp);
        }
    }

    /*
     * 测试一下,输出3 2 -1。成功!
     */
    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(3);
        lruCache.set(1, 2);
        lruCache.set(2, 3);
        lruCache.set(3, 4);
        System.out.println(lruCache.get(2));
        lruCache.set(2, 2);
        lruCache.set(4, 5);
        System.out.println(lruCache.get(2));
        System.out.println(lruCache.get(1));
    }

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值