前言

  • 相信有的伙伴在面试的过程中,或多或少的会被问到redis的内存淘汰策略,可能大部分人都知道都有哪些对应的策略,毕竟对于八股文的套路大家肯定早已铭记于心。但是当面试官问你如何实现或者让你去写一个对应策略的算法时,可能就顿时一脸蒙蔽了:不对啊,套路不是这样的啊!!如果单纯的让你直接写对应的算法还好,要是再更深入一点让你说一下你的思考过程或者说如果让你来设计你会怎么去做,这个可能就上升到了一个架构的思维(如果你打算面试架构死,不妨提前锻炼一下这样的思维),对于平时没有这方面准备的伙伴来讲,那无疑就是当头一棒:与自己心仪的offer就要失之交臂了。
  • 这篇文章我将从如何设计LRU Cache算法,跟大家一起来看一下整体的设计过程,包括我们应该如何去思考,如何去解决,提前锻炼一下自己初步的架构设计能力。当然在这里只是浅谈一下我的思考过程,为大家抛砖引玉,欢迎在留言区分享你的想法。目前博主个人博客已经搭建发布,后期相关文章也会发布在上面,大家有兴趣可以去上面学习,点击即可前往文青乐园

LRU Cache基本概述

LRU 是什么

  • 基本概述
    LRU(Least Recently Used) ,即最近最少使用,它是一种缓存逐出策略 cache eviction policies。LRU 算法是假设最近最少使用的那些信息,将来被使用的概率也不大,所以在容量有限的情况下,就可以把这些不常用的信息清理掉。
  • 使用场景
    比如有热点新闻时,所有人都在搜索这个信息,那刚被一个人搜过的信息接下来被其他人搜索的概率也大,之前的新闻被搜索的概率就会小,所以我们把很久没有用过的信息清理掉,也就是把 Least Recently Used 的信息清理掉。

我们举个例子,假设内存容量为 5,现在有 1-5 五个数,存储顺序如下:

数据从左到右边其使用最新程度逐渐增加,比如1就是最早使用的数据,5就是最近使用的数据,我们现在想加入一个新的数6,可是容量已经满了,所以需要清理其中的某一个,那按照什么规则清理呢?目前主要有如下缓存逐出策略:

  • LFU (Least Frequently Used) :这个是计算每个信息的访问次数,清除掉访问次数最少的那个;如果访问次数一样,就清除掉好久没用过的那个。这个算法其实很高效,但是耗资源,所以一般不用。
  • LRU (Least Recently Used) :这是目前最常用了,把很长时间没有用过的清除掉,那它的隐含假设就是,认为最近用到的信息以后用到的概率会更大。

那我们这个例子中就是把最老的 1 清理掉,变成:

如此不断地进行迭代。

Cache 是什么

  • 基本概述

    • 简单理解就是:把一些可以重复使用的信息存起来,以便之后需要时可以快速拿到。至于它存在哪里就不一定了,最常见的是存在内存里,也就是 memory cache,但也可以不存在内存里。
  • 使用场景
    • Spring 中有 @Cacheable 等支持 Cache 的一系列注解,使用它大大减少了 call 某服务器的次数,解决了一个性能上的问题。
    • 在进行数据库查询的时候,不想每次请求都去 call 数据库,那我们就在内存里存一些常用的数据,来提高访问性能。这种设计思想其实是遵循了著名的“二八定律”。在读写数据库时,每次的 I/O 过程消耗很大,但其实 80% 的 request 都是在用那 20% 的数据,所以把这 20% 的数据放在内存里,就能够极大的提高整体的效率。

总之,Cache 的目的是存一些可以复用的信息,方便将来的请求快速获得。

那我们知道了 LRU,了解了 Cache,合起来就是 LRU Cache 了:当 Cache 储存满了的时候,使用 LRU 算法把老数据清理出去。接下来我们看看具体的实现思路。

LRU Cache设计思路详解

其实很多伙伴都知道设计这个算法需要使用 HashMap + Doubly Linked List,或者说用 Java 中现成的 LinkedHashMap,但是,你有思考过下面的问题么?

  • 为什么是使用上面的数据结构?
  • 你是怎么想到用这两个数据结构的?

如果真的问到这个,面试的时候不讲清楚这个,不说清楚思考过程,代码写对了也没用。其实这个和在工作中的设计思路类似,没有人会告诉我们要用什么数据结构,一般的思路是:

  1. 先想有哪些操作
  2. 然后根据这些操作,再去看哪些数据结构合适
  3. 定义数据结构内容

接下来我们就从上面的思路出发进行设计。

分析 Operations

对于这个 LRU Cache 需要有哪些操作呢?我们来分析一下:

  1. 首先最基本的操作就是能够从里面读信息,不然之后快速获取是咋来的;
  2. 还得能加入新的信息,新的信息进来就是 most recently used 了;
  3. 在加新信息之前,还得看看有没有空位,如果没有空间了,得先把老的清理掉,那就需要能够找到那个老的数据并且删除它;
  4. 如果加入的新信息是缓存里已经有的,那意思就是 key 已经有了,要更新 value,那就只需要调整一下这条信息的 priority,将它从上一次被使用升级为最新使用。

找寻数据结构

第一个操作很明显,我们需要一个能够快速查找的数据结构,非 HashMap 莫属,还不了解 HashMap 原理和设计规则的大家可以去百度查查,这里不做赘述。可是发现后面的操作 HashMap 就不顶用了。这里我们先来数一遍基本的数据结构:Array, LinkedList, Stack, Queue, Tree, BST, Heap, HashMap。在做这种数据结构的题目时,就这样把所有的数据结构列出来,一个个来分析,有时候不是因为这个数据结构不行,而是因为其他的数据结构更好。我们做出如下的分析:

  • Array, Stack, Queue :这三种本质上都是 Array 实现的(当然 Stack, Queue 也可以用 LinkedList 来实现。。),一会插入新的,一会删除老的,一会调整下顺序,array 不是不能做,时间复杂度 O(n) 啊,不可行;
  • BST :同理,时间复杂度是 O(logn);
  • Heap: 即便可以,也是 O(logn);
  • LinkedList:有点可以哦,按照从老到新的顺序,排排站,删除、插入、移动,都可以是 O(1) 。但是删除时我还需要一个 previous pointer 才能删掉,所以我需要一个 Doubly LinkedList。

最后我们数据结构选定为HashMap + Double LinkedList。

定义数据结构的内容

选好了数据结构之后,还需要定义清楚每个数据结构具体存储的是是什么,HashMap + Doubly LinkedList两个数据结构是如何联系的,这才是核心问题。我们先想个场景,在搜索引擎里,你输入问题 Questions,谷歌给你返回答案 Answer。那我们就先假设这两个数据结构存的都是 <Q, A>,现在我们的 HashMap 和 LinkedList 长这样:

然后我们进行以下操作:

  • 直接从 HashMap 里读取 Answer 即可,O(1),没问题;
  • 新加入一组 Q&A,两个数据结构都得加,那先要判断一下当前的缓存里有没有这个 Q,那我们用 HashMap 判断,如果没有这个 Q,加进来,都没问题;
  • 如果已经有这个 Q,HashMap 这里要更新一下 Answer,然后我们还要把 LinkedList 的那个 node 移动到最后或者最前,因为它变成了最新被使用的了嘛。

可是,怎么找 LinkedList 的这个 node 呢?一个个遍历去找并不是我们想要的,因为要 O(n) 的时间嘛,我们想用 O(1) 的时间操作。那也就是说这样记录是不行的,还需要记录 LinkedList 中每个 ListNode 的位置,这就是设计的关键所在。怎么设计呢?自然是在 HashMap 里记录 ListNode 的位置这个信息了,也就是存一下每个 ListNode 的 reference。想想其实也是,HashMap 里没有必要记录 Answer,Answer 只需要在 LinkedList 里记录就可以了。之后我们更新、移动每个 node 时,它的 reference 也不需要变,所以 HashMap 也不用改动,动的只是 previous, next pointer。那再一想,其实 LinkedList 里也没必要记录 Question,反正 HashMap 里有。这两个数据结构是相互配合来用的,不需要记录一样的信息。更新后的数据结构如下:

这样,我们才分析出来用什么数据结构,每个数据结构里存的是什么,物理意义是什么。
那我们再用图来总结一下:

画图的时候边讲边写,每一步都从 high level 到 detail 再到代码,把代码模块化。

  • 比如“Welcome”是要把这个新的信息加入到 HashMap 和 LinkedList 里,那我会用一个单独的 add() method 来写这块内容,那在下面的代码里我取名为 appendHead(),更精准;
  • “踢走老的”这里我也是用一个单独的 remove() method 来写的。

有了上面的分析,接下里直接给出设计的代码。

LRU Cache算法实现

class LRUCache {// HashMap: <key = Question, value = ListNode>// LinkedList: <Answer>public static class Node {int key;int val;Node next;Node prev;public Node(int key, int val) {this.key = key;this.val = val;}}Map<Integer, Node> map = new HashMap<>();private Node head;private Node tail;private int cap;public LRUCache(int capacity) {cap = capacity;}public int get(int key) {Node node = map.get(key);if(node == null) {return -1;} else {int res = node.val;remove(node);appendHead(node);return res;}}public void put(int key, int value) {// 先 check 有没有这个 keyNode node = map.get(key);if(node != null) {node.val = value;// 把这个node放在最前面去remove(node);appendHead(node);} else {node = new Node(key, value);if(map.size() < cap) {appendHead(node);map.put(key, node);} else {// 踢走老的map.remove(tail.key);remove(tail);appendHead(node);map.put(key, node);}}}private void appendHead(Node node) {if(head == null) {head = tail = node;} else {node.next = head;head.prev = node;head = node;}}private void remove(Node node) {if(head == tail) {head = tail = null;} else {if(head == node) {head = head.next;node.next = null;} else if (tail == node) {tail = tail.prev;tail.next = null;node.prev = null;} else {node.prev.next = node.next;node.next.prev = node.prev;node.prev = null;node.next = null;}}}}/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/

如何设计LRU Cache算法相关推荐

  1. linux cache lru回收,LRU cache 算法

    上周末同学问了一些操作系统的问题,涉及到LRU cache,顺便复习了一下. LRU是least recently used的缩写,意思是最近最少使用,是一种内存页面置换算法.根据程序设计局部性的原则 ...

  2. 保研面试 算法题_面试挂在了一道 LRU 缓存算法设计题

    前言 好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了. 当时面试官问到这个题的时候,很快给了一个思路,但是手写的时候,发现自己没有深刻理解它,加上当 ...

  3. 常考数据结构和算法:设计LRU缓存结构

    题目描述 设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能 set(key, value):将记录(key, value)插入该结构 get(key):返回key对应的val ...

  4. 一种简单的LRU cache设计 C++

    最近在工作中需要用到LRU cache用作缓存来提高性能,经过查阅各种资料,了解了其运行的机制,如下: LRU cache可以用于在内存中保持当前的热点数据,下面实现一个有大小限制的lru cache ...

  5. 单机 “5千万以上“ 工业级 LRU cache 实现

    文章目录 前言 工业级 LRU Cache 1. 基本架构 2. 基本操作 2.1 insert 操作 2.2 高并发下 insert 的一致性/性能 保证 2.3 Lookup操作 2.4 shar ...

  6. 代码写对了还挂了?程序媛小姐姐从 LRU Cache 带你看面试的本质

    来源 | 码农田小齐 责编 |  Carol 前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码 ...

  7. 从 LRU Cache 带你看面试的本质

    前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码大家都会写,那面试为什么还在考这些题?那为什么 ...

  8. 实现 LRU 缓存算法

    1 LRU 缓存介绍 LRU 算法全称是最近最少使用算法(Least Recently Use),是一种简单的缓存策略.顾名思义,LRU 算法会选出最近最少使用的数据进行淘汰. 那么什么是缓存呢?缓存 ...

  9. 【高阶数据结构】LRU Cache

    文章目录 什么是LRU Cache LRU Cache的实现 LRU缓存OJ: 结构设计分析 list的**splice函数**介绍: get函数的实现: put函数的实现: 整体代码: 什么是LRU ...

最新文章

  1. 腾讯医疗AI新突破:提出器官神经网络,全自动辅助头颈放疗规划 | 论文
  2. 设计模式 - Iterator(迭代器)
  3. ffplay 分析(音频从Frame(解码后)队列取数据到SDL输出)
  4. 云上救命APP!——e代驾手机客户端!
  5. QT 线程池 + TCP 小试(一)线程池的简单实现
  6. 语义级代码克隆检测数据集的评估与改进
  7. 服务器java项目转移到另一个盘_将svn的项目从一台服务器转移到另外一台服务器...
  8. ad16自动布线设置规则_未来的PCB协同设计制造过程离不开自动化工具
  9. MacOS Big Sur 11.4 (20F71) OC 0.7.0 / Cl 5135 / PE 三分区原版黑苹果镜像
  10. [网络安全自学篇] 四十八.Cracer第八期——(1)安全术语、Web渗透流程、Windows基础、注册表及黑客常用DOS命令
  11. PcShare2005控制端修改
  12. Stack (算法入门4)
  13. win10系统怎么找服务器地址,win10系统下如何快速查找本地ip地址
  14. 联通云服务器怎么修改密码,桌面云使用说明
  15. 【深度学习】深度学习入门:投身深度学习你需要哪些准备?
  16. [天池竞赛系列] 历届天池竞赛答辩PPT和视频
  17. web服务之LAMPLNMP架构
  18. 显卡功耗天梯图 2023年显卡功耗排行天梯图
  19. 在excel中如何筛选重复数据_Excel 在大量数据中快速筛选出重复数据
  20. HDU4417_树状数组加离线

热门文章

  1. GPTEE中定义的RSA的Algorithm Identifier详解
  2. 正则表达式,解决要么有要有没有,但必须开头
  3. 使用资源文件绘制Win32对话框
  4. 返回对象1(未调用拷贝构造函数)
  5. 【网络安全】SQL注入攻击思路手法总结(上)
  6. 【网络安全】针对 HTTP/2 协议的HTTP Desync攻击
  7. 分享SSRF漏洞的学习和利用
  8. 11、修改和删除索引(DROP INDEX)
  9. 过滤输入字符串中的危险字符
  10. CSS入门之【背景图像和渐变】