头条面试高频题目,手撕 LRU - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
Acceml
V2EX    程序员

头条面试高频题目,手撕 LRU

  •  
  •   Acceml
    Acceml 2019-03-09 10:53:34 +08:00 8488 次点击
    这是一个创建于 2411 天前的主题,其中的信息可能已经有所发展或是发生改变。

    题目

    运用你所掌握的数据结构,设计和实现一个 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 

    题解

    这道题在今日头条、快手或者硅谷的公司中是比较常见的,代码要写的还蛮多的,难度也是 hard 级别。

    最重要的是 LRU 这个策略怎么去实现, 很容易想到用一个链表去实现最近使用的放在链表的最前面。 比如 get 一个元素,相当于被使用过了,这个时候它需要放到最前面,再返回值, set 同理。 那如何把一个链表的中间元素,快速的放到链表的开头呢? 很自然的我们想到了双端链表。

    基于 HashMap 和 双向链表实现 LRU 的

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

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

    下面展示了,预设大小是 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 双向链表部分变化如下: image

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

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

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

     private static class DLinkedNode { int key; int value; DLinkedNode pre; DLinkedNode post; } /** * 总是在头节点中插入新节点. */ private void addNode(DLinkedNode node) { node.pre = head; node.post = head.post; head.post.pre = node; head.post = node; } /** * 摘除一个节点. */ private void removeNode(DLinkedNode node) { DLinkedNode pre = node.pre; DLinkedNode post = node.post; pre.post = post; post.pre = pre; } /** * 摘除一个节点,并且将它移动到开头 */ private void moveToHead(DLinkedNode node) { this.removeNode(node); this.addNode(node); } /** * 弹出最尾巴节点 */ private DLinkedNode popTail() { DLinkedNode res = tail.pre; this.removeNode(res); return res; } private HashMap<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>(); 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; // cache 里面没有 } // cache 命中,挪到开头 this.moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node = cache.get(key); 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) { // 最后一个节点弹出 DLinkedNode tail = this.popTail(); this.cache.remove(tail.key); count--; } } else { // cache 命中,更新 cache. node.value = value; this.moveToHead(node); } } 

    热门阅读

    Leetcode 名企之路

    28 条回复    2019-03-11 00:27:13 +08:00
    xylophone21
        1
    xylophone21  
       2019-03-09 11:29:51 +08:00   24
    这个场景下,你们把 key 翻译成密钥?
    cxtrinityy
        2
    cxtrinityy  
       2019-03-09 11:37:13 +08:00   2
    既然已经用了 HashMap,直接 LinkedHashMap 可破,不需要自己再去实现链表吧
    count > capacity 时,你需要 foreach map 逐个排除,直至 count 降到 capacity 以下,这里是以个数为准,所以直接踢掉 tail 也可以
    gethermyp
        3
    gethermyp  
       2019-03-09 13:06:05 +08:00
    直接用 linkedhashmap
    swulling
        4
    swulling  
       2019-03-09 13:07:06 +08:00 via iPhone   1
    为什么把 Key 翻译为密钥?不是键么
    HarryQu
        5
    HarryQu  
       2019-03-09 13:18:58 +08:00   1
    lazydog
        6
    lazydog  
       2019-03-09 13:22:51 +08:00 via Android
    看到 LRU,我就只知道要用 HashMap 和双向链表实现,但具体怎么实现,不造~
    ecrazy
        7
    ecrazy  
       2019-03-09 15:43:03 +08:00 via iPhone
    这样直接抄没事?
    CSM
        8
    CSM  
       2019-03-09 15:46:13 +08:00
    @HarryQu #5 你好,看了这个代码有个疑问,就是 get 的实现中在命中 cache 后,貌似并没有将其移到 LinkedHashMap 的末尾?
    pythondean
        9
    pythondean  
       2019-03-09 16:01:03 +08:00
    https://github.com/golang/groupcache/blob/master/lru/lru.go
    golang 团队在 groupcache 中使用到了 lru
    HarryQu
        10
    HarryQu  
       2019-03-09 16:28:00 +08:00
    要理解 LruCache, 首先你要了解 HaspMap 原理,

    然后理解 LinkedHashMap 原理, 再然后理解 LinkedHashMap 中的 accessOrder 属性 。

    节点的移动是在 LinkedHashMap 中的 get 方法中 ,以 JDK 1.8 为例 , 方法调用为 :
    LinkedHashMap.get() => accessOrder 为 true => afterNodeAccess() 中 移动节点

    其中 accessOrder 是 LruCache 在 LinkedHashMap 初始化的时候设置为 true .

    因个人水平有限 , 具体的源码分析就不做阐述了,麻烦你 Google 一下 。

    需要注意一点的是在 JDK 1.7 和 JDK 1.8 的源码中 ,HashMap 内部实现不同 。
    HarryQu
        11
    HarryQu  
       2019-03-09 16:28:54 +08:00
    @CSM 忘记 @ 你了 你看下 10 楼
    CSM
        12
    CSM  
       2019-03-09 16:37:26 +08:00
    @HarryQu #10 原来 LinkedHashMap 的迭代除了插入顺序,还可以是访问顺序,现在明白了。非常感谢。
    BBCCBB
        13
    BBCCBB  
       2019-03-09 20:47:57 +08:00
    @cxtrinityy
    @gethermyp
    两位老哥, 虽然楼主一直发公众号很烦, 但是这个面试的时候要你自己写一个 LRU,你说直接用 linkedhashmap,这不就 GG 了, ==
    Actrace
        14
    Actrace  
       2019-03-09 21:06:57 +08:00
    我觉得面试要手写 LRU 的公司不去也罢。
    cxtrinityy
        15
    cxtrinityy  
       2019-03-09 21:18:22 +08:00
    @BBCCBB 没有啊,你既然都用了 HashMap 本身就表示接受 JDK 类了,HashMap 本身也是优化后的实现,既然不能用 LinkedHashMap,那么就不能用 HashMap,那么文章里就应该用数组自己去实现 Hash 存储
    hanxiV2EX
        16
    hanxiV2EX  
       2019-03-09 21:25:55 +08:00 via Android
    在电脑上写过,知道原理就很好写出来。hash 表和双端链表配合。get 或者 set 之后就把这个节点放到 head,set 时判断下 size,超过了就把 tail 删掉。
    hanxiV2EX
        17
    hanxiV2EX  
       2019-03-09 21:27:38 +08:00 via Android
    LRU 在游戏服务端开发很有用的,把活跃数据缓存到内存中。
    BBCCBB
        18
    BBCCBB  
       2019-03-09 22:12:20 +08:00
    @cxtrinityy 我觉得这个还是主要考怎么实现一个 lru,主要是思路,就是将双端队列和 hashmap 结合起来的思路,并不会说让实现一个 hashmap 的。或者你直接照着 linkedhashmap 的原理说。
    woscaizi
        19
    woscaizi  
       2019-03-09 22:17:46 +08:00 via iPhone
    @Actrace 蚂蚁伯乐系统面试里就有这题。
    zpxshl
        20
    zpxshl  
       2019-03-09 23:33:47 +08:00 via Android
    真巧面头条时确实遇到...虽然知道原理但一时脑抽说写不出来...我后面也在纳闷为什么我会觉得写不出来
    miaobug
        21
    miaobug  
       2019-03-10 00:03:37 +08:00
    python 的 OrderedDict 了解一下,几行写完 2333
    20015jjw
        22
    20015jjw  
       2019-03-10 02:45:34 +08:00 via Android
    看了一下周围的朋友没有关注这个公众号 我就放心了
    lz 写了这么多帖子还是没长进啊...
    DavidNineRoc
        23
    DavidNineRoc  
       2019-03-10 11:44:46 +08:00
    不应该看一下 LFU 这个比 LRU 高级一点
    pathbox
        24
    pathbox  
       2019-03-10 12:31:50 +08:00 via iPhone   1
    @lazydog 简单理解:hashmap 做缓存层,为了读操作,效率是 O(1) 链表是真正的存储层,写操作数据是操作链表,然后再把链表的数据和 hashmap 同步,包括删除的同步 热数据放链表头,冷数据自然会在尾部了超过 size 时,在尾部删除多出的数据
    cxtrinityy
        25
    cxtrinityy  
       2019-03-10 12:32:57 +08:00 via Android
    @BBCCBB 会用 linkedHashmap 去实现本身就说明理解了思路吧,不然问为什么用说不出来不是一样么
    lazydog
        26
    lazydog  
       2019-03-10 13:04:54 +08:00 via Android
    @pathbox 非常感谢你的解释~
    zclHIT
        27
    zclHIT  
       2019-03-10 21:56:11 +08:00
    哈工大校友前来帮顶(●''●)
    darkTianTian
        28
    darkTianTian  
       2019-03-11 00:27:13 +08:00
    没有让你实现`超时淘汰`功能吗??
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3032 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 00:26 PVG 08:26 LAX 17:26 JFK 20:26
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86