首发公众号:bigsai 转载请放置作者和原文(本文)链接

前言

大家好,我是bigsai,好久不见,甚是想念!

最近有个小伙伴跟我诉苦,说他没面到LRU,他说他很久前知道有被问过LRU的但是心想自己应该不会遇到,所以暂时就没准备。

奈何不巧,这还就真的考到了!他此刻的心情,可以用一张图来证明:

他说他最终踉踉跄跄的写了一个效率不是很高的LRU,面试官看着不是很满意……后来果真GG了。

防止日后再碰到这个坑,今天和大家一起把这个坑踩了,这道题我自身刚开始也是用较为普通的方法,但是好的方法虽然不是很难但是想了真的很久才想到,虽然花了太多时间不太值,总算是自己想出来了,将这个过程给大家分享一下(只从算法的角度,不从操作系统的角度)。

理解LRU

设计一个LRU,你得知道什么是LRU吧?

LRU,英文全称为Least Recently Used,翻译过来就是最近最久未使用算法,是一种常用的页面置换算法

说起页面置换算法,这就是跟OS关系比较大的了,我们都知道内存的速度比较快,但是内存的容量是非常有限的,不可能给所有页面装到内存中,所以就需要一个策略将常用的页面预放到内存中。

但是吧,谁也不知道进程下次会访问哪个内存,并不能很有效的知道(我们在当前并没有预测未来的功能),所以有些页面置换算法只是理想化但是没法真实实现的(没错就是最佳置换算法(Optimal)),然后常见必回的算法就是FIFO(先进先出)和LRU(最近最久未使用)。

LRU理解不难,就是维护一个有固定大小的容器,核心就是get()和put()两个操作。

我们先看一下LRU会有的两个操作:

初始化:LRUCache(int capacity) ,以正整数作为容量 capacity 初始化 LRU 缓存。

查询:get(int key),从自己的设计的数据结构中查找是否有当前key对应的value,如果有那么返回对应值并且要将key更新记录为最近使用,如果没有返回-1。

插入/更新:put(int key,int value),可能是插入一个key-value,也可能是更新一个key-value,如果容器中已经存才这个key-value那么只需要更新对应value值,并且标记成最新。如果容器不存在这个值,那么要考虑容器是否满了,如果满了要先删除最久未使用的那对key-value。

这里的流程可以给大家举个例子,例如

容量大小为2:
[ "put", "put", "get", "put","get", "put","get","get","get"]
[ [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]

这个过程如下:

大家容易忽略的细节有:

  • put()存在更新的操作,例如put(3,3),put(3,4)会更新key为3的操作。
  • get()可能查询不到,但是查询到也会更新最久未使用的顺序
  • 如果容器未使用满,那么put可能更新可能插入,但是不会删除;如果容器满了并且put插入,就要考虑删除最久未使用的key-value了。

对于上面的这么一个规则,我们该如何处理呢?

如果单单用一个List类似的列表,可以顺序存储键值对,在List前面的(0下标为前)我们认为它是比较久的,在List后我们认为它是比较新的。我们考虑下各种操作可能会这样设计:

如果来get操作:

遍历List一个个比对,查看是否有该key的键值对,如果有直接返回对应key的value,如果没有那么返回-1.

如果来put操作:

遍历List,如果有该key的键值对,那么果断删除这个key-value,最后在末尾统一插入该键值对。

如果没有对应的key并且List容器已经到达最满了,那么果断删除第一个位置的key-value。

用List可能需要两个(一个存key一个存value),或者一个存Node节点(key,value为属性)的List,考虑下这个时间复杂度:

put操作:O(n),get操作:O(n) 两个操作都需要枚举列表线性复杂度,效率属实有点拉胯,肯定不行,这样的代码我就不写了。

哈希初优化

从上面的分析来看,我们已经可以很自信的将LRU写出来了,不过现在要考虑的是一个优化的事情。

如果说我们将程序中引入哈希表,那么肯定会有一些优化的。用哈希表存储key-value,查询是否存在的操作都能优化为O(1),但是删除或者插入或者更新位置的复杂度可能还是O(n),我们一起分析一下:

最久未使用一定是一个有序的序列来储存,要么是顺序表(数组)要么是链表,如果是数组实现的ArrayList存储最久未使用这个序列。

如果是ArrayList进行删除最久未使用(第一个)key-value,新的key被命中变成最新被使用(先删除然后插入末尾)操作都是O(n)。

同理如果是LinkedList的一些操作大部分也是O(n)的,像删除第一个元素这个是因为数据结构原因O(1)。

你发现自己的优化空间其实非常非常小,但是确实还是有进步的,只是被卡住不知道双O(1)的操作究竟怎么优化,这里面我把这个版本代码放出来,大家可以参考一下(如果面试问到实在不会可以这么写)

class LRUCache {

    Map<Integer,Integer>map=new HashMap<>();
List<Integer>list=new ArrayList<>();
int maxSize;
public LRUCache(int capacity) {
maxSize=capacity;
} public int get(int key) {
if(!map.containsKey(key))//不存在返回-1
return -1;
int val=map.get(key);
put(key,val);//要更新位置 变成最新 很重要!
return val;
} public void put(int key, int value) {
//如果key存在,直接更新即可
if (map.containsKey(key)) {
list.remove((Integer) key);
list.add(key);
} else {//如果不存在 要插入到最后,但是如果容量满了需要删除第一个(最久)
if (!map.containsKey(key)) {
if (list.size() == maxSize) {
map.remove(list.get(0));
list.remove(0);
}
list.add(key);
}
}
map.put(key, value);
}
}

哈希+双链表

上面我们已经知道用哈希能够直接查到有木有这个元素,但是苦于删除!用List都很费力。

更详细的说,是苦于List的删除操作,Map的删除插入还是很高效的。

在上面这种情况,我们希望的就是能够快速删除List中任意一个元素,并且效率很高,如果借助哈希只能最多定位到,但是无法删除啊!该怎么办呢?

哈希+双链表啊!

我们将key-val的数据存到一个Node类中,然后每个Node知道左右节点,在插入链表的时候直接存入Map中,这样Map在查询的时候可以直接返回该节点,双链表知道左右节点可以直接将该节点在双链表中删除。

当然,为了效率,这里实现的双链表带头结点(头指针指向一个空节点防止删除等异常情况)和尾指针。

对于这个情况,你需要能够手写链表和双链表啦,双链表的增删改查已经写过清清楚楚,小伙伴们不要担心,这里我已经整理好啦:

单链表:https://mp.weixin.qq.com/s/Cq98GmXt61-2wFj4WWezSg

双链表:https://mp.weixin.qq.com/s/h6s7lXt5G3JdkBZTi01G3A

也就是你可以通过HashMap直接得到在双链表中对应的Node,然后根据前后节点关系删除,期间要考虑的一些null、尾指针删除等等特殊情况即可。

具体实现的代码为:

class LRUCache {
class Node {
int key;
int value;
Node pre;
Node next; public Node() {
} public Node( int key,int value) {
this.key = key;
this.value=value;
}
}
class DoubleList{
private Node head;// 头节点
private Node tail;// 尾节点
private int length;
public DoubleList() {
head = new Node(-1,-1);
tail = head;
length = 0;
}
void add(Node teamNode)// 默认尾节点插入
{
tail.next = teamNode;
teamNode.pre=tail;
tail = teamNode;
length++;
}
void deleteFirst(){
if(head.next==null)
return;
if(head.next==tail)//如果删除的那个刚好是tail 注意啦 tail指针前面移动
tail=head;
head.next=head.next.next; if(head.next!=null)
head.next.pre=head;
length--;
}
void deleteNode(Node team){ team.pre.next=team.next;
if(team.next!=null)
team.next.pre=team.pre;
if(team==tail)
tail=tail.pre;
team.pre=null;
team.next=null;
length--;
}
public String toString() {
Node team = head.next;
String vaString = "len:"+length+" ";
while (team != null) {
vaString +="key:"+team.key+" val:"+ team.value + " ";
team = team.next;
}
return vaString;
}
}
Map<Integer,Node> map=new HashMap<>();
DoubleList doubleList;//存储顺序
int maxSize;
LinkedList<Integer>list2=new LinkedList<>(); public LRUCache(int capacity) {
doubleList=new DoubleList();
maxSize=capacity;
}
public void print(){
System.out.print("maplen:"+map.keySet().size()+" ");
for(Integer in:map.keySet()){
System.out.print("key:"+in+" val:"+map.get(in).value+" ");
}
System.out.print(" ");
System.out.println("listLen:"+doubleList.length+" "+doubleList.toString()+" maxSize:"+maxSize);
} public int get(int key) {
int val;
if(!map.containsKey(key))
return -1;
val=map.get(key).value;
Node team=map.get(key);
doubleList.deleteNode(team);
doubleList.add(team);
return val;
} public void put(int key, int value) {
if(map.containsKey(key)){// 已经有这个key 不考虑长短直接删除然后更新
Node deleteNode=map.get(key);
doubleList.deleteNode(deleteNode);
}
else if(doubleList.length==maxSize){//不包含并且长度小于
Node first=doubleList.head.next;
map.remove(first.key);
doubleList.deleteFirst();
}
Node node=new Node(key,value);
doubleList.add(node);
map.put(key,node); }
}

就这样,一个get和put都是O(1)复杂度的LRU写出来啦!

尾声

后来看了题解,才发现,Java中的LinkedHashMap也差不多是这种数据结构!几行解决,但是一般面试官可能不会认同,还是会希望大家能够手写一个双链表的。

class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity; public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
} public int get(int key) {
return super.getOrDefault(key, -1);
} public void put(int key, int value) {
super.put(key, value);
} @Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}

哈希+双链表虽然在未看题解的情况想出来,但是真的花了挺久才想到这个点,以前见得确实比较少,高效手写LRU到今天算是真真正正的完全掌握啦!

不过除了LRU,其他的页面置换算法无论笔试还是面试也是非常高频啊,大家有空自己梳理一下哦。

个人技术公众号:bigsai ,定期分享,欢迎一起打卡力扣、学习交流!

字节面试问我如何高效设计一个LRU,当场懵的更多相关文章

  1. 如何设计一个LRU Cache

    如何设计一个LRU Cache? Google和百度的面试题都出现了设计一个Cache的题目,什么是Cache,如何设计简单的Cache,通过搜集资料,本文给出个总结. 通常的问题描述可以是这样: Q ...

  2. 面试系列 30 如何自己设计一个类似dubbo的rpc框架

    其实一般问到你这问题,你起码不能认怂,因为既然咱们这个课程是短期的面试突击训练课程,那我不可能给你深入讲解什么kafka源码剖析,dubbo源码剖析,何况我就算讲了,你要真的消化理解和吸收,起码个把月 ...

  3. 常见面试算法题JS实现-设计一个有getMin功能的栈

    前言: 已经确定工作了-下周一正式入职,按理说应该是可以好好浪荡一周的,但是内心总是不安,总觉得自己这个水平真的太菜了,还是趁着现在有自己的时间,赶紧多看看书,多学习学习吧orz所以把之前校招买的书, ...

  4. 8.如何自己设计一个类似 Dubbo 的 RPC 框架?

    作者:中华石杉 面试题 如何自己设计一个类似 Dubbo 的 RPC 框架? 面试官心理分析 说实话,就这问题,其实就跟问你如何自己设计一个 MQ 一样的道理,就考两个: 你有没有对某个 rpc 框架 ...

  5. 《程序员代码面试指南》第一章 栈和队列 设计一个有getMin功能的栈

    题目 实现一个特殊的栈,在实现栈的基本功能上,再实现返回栈中最小的元素的操作 要求 1. pop.push.getMin操作时间复杂度都是O(1) 2. 设计的栈类型可以使用现成的栈结构 java代码 ...

  6. 高并发架构系列:如何从0到1设计一个类Dubbo的RPC框架

    在过去持续分享的几十期阿里Java面试题中,几乎每次都会问到Dubbo相关问题,比如:“如何从0到1设计一个Dubbo的RPC框架”,这个问题主要考察以下几个方面: 你对RPC框架的底层原理掌握程度. ...

  7. 2019Android阿里&腾讯&百度&字节面试汇总(附面试题总结、Android书单)

    1.基本情况 先简单说说我今年的面试经历吧,本人2018届211软件工程硕士生,Android开发岗.此文主要是2019年年初春招的面试和秋招面试经验汇总,最终拿到了阿里,腾讯,字节跳动,百度等off ...

  8. 如何设计一个RPC系统

    版权声明:本文由韩伟原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/162 来源:腾云阁 https://www.qclou ...

  9. 转载:SQL Server高效 -- 设计(ITPUT 讨论汇总

    http://blog.csdn.net/zjcxc/article/details/8979756 认为在设计SQL Server对象时,主要会考虑哪些因素来避免出现性能问题? 讨论汇总——总体设计 ...

随机推荐

  1. 绝世好题(DP)

    题目链接:绝世好题 暴力就不用说了,和lis神似,O(n2)妥妥的挂掉,但可以得大部分分(好像是90,80)... 考虑优化,来一发非正解的优化: #include<bits/stdc++.h& ...

  2. freeswitch的docker构建过程

    概述 Docker是一个开源的应用容器引擎,可以让开发者打包应用以及依赖包到一个轻量级.可移植的容器中,并在任何安装有Docker的机器上运行. Docker 使你能够将应用程序与基础架构分开,从而可 ...

  3. redhat 7.x 的防火墙软件firewall 介绍

    zone 的概念.firewall 一般有9个zone ,配置文件都在 /usr/lib/firewalld/zones/ 里面. 系统的配置文件目录就在 /usr/lib/firewalld 这个目 ...

  4. centos7 二进制安装mysql-8.0.19

    安装包下载地址:https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.19-linux-glibc2.12-x86_64.tar.xz 1.检 ...

  5. Haar小波的理解

    1. 首先理解L^2(R)的概念 L^2(R) 是一个内积空间的概念,表示两个无限长的向量做内积,张成的空间问题.也就是两个函数分别作为一个向量,这两个函数要是平方可积的.L^2(a,b)=<f ...

  6. 怎么将本地已有的一个项目上传到新建的git仓库的方法

    将本地已有的一个非git项目上传到新建的git仓库的方法一共有两种. 一. 克隆+拷贝 第一种方法比较简单,直接用把远程仓库拉到本地,然后再把自己本地的项目拷贝到仓库中去.然后push到远程仓库上去即 ...

  7. 【完虐算法】LeetCode 接雨水问题,全复盘

    大家好! 动态规划题目是总结的比较完整了.下面是自从和大家刷开题总结的动态规划解题方法. 今年全国夏天雨是真的多,突然想到今年北京的夏天也不像往年那么热.不知不觉就稳稳地度过了夏天来到秋天. 恰巧前几 ...

  8. Android 有意思的脚本(打印温度)

    https://github.com/LineageOS/android_hardware_google_pixel/blob/lineage-18.1/thermal/device.mk #!/sy ...

  9. 纯前端实现词云展示+附微博热搜词云Demo代码

    前言 最近工作中做了几个数据可视化大屏项目,其中也有用到了词云展示,以前做词云都是用python库来生成图片显示的,这次用了纯前端的实现(Ctrl+V真好用),同时顺手做个微博热搜的词云然后记录一下~ ...

  10. python一对一教程:Computational Problems for Physics chapter 1 Code Listings

    作者自我介绍:大爽歌, b站小UP主 ,直播编程+红警三 ,python1对1辅导老师 . 本博客为一对一辅导学生python代码的教案, 获得学生允许公开. 具体辅导内容为<Computati ...