LRU 居然翻译成最近最少使用?真相原来是这样!(附力扣题解)
前言
相信有很多同学和我一样,第一次碰到 LRU(Least Recently Used) 的这个解释「最近最少使用」都不知道是什么意思,用汤家凤老师的话来说:
我真的感到匪夷所思啊!
最近是表示时间,最少是表示频度,拆开来都知道,但是合在一起就不知道是什么意思了。经过一番搜索后,我发现这可能是国内一些专业名词的通病:翻译问题。甚至百度百科对 LRU 的解释也是这样:
LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
什么叫「选择最近最久未使用的页面予以淘汰」?今天我们就来摸一摸它的底!
LRU 算法过程
LRU 常见的实现是使用一个双向链表保存缓存数据,算法步骤如下:
- 插入新数据时会插入到链表头部;
- 当缓存数据被访问时,将该数据移到链表尾部;
- 当链表满的时候,将链表头部的数据丢弃。
看完算法过程,内心惊呼:这不就是当容量不够用的时候淘汰最久没访问的数据么,和最少有个毛关系啊!按我的理解,Least Recently 其实应该翻译成 「最不是最近的」也就是最远的,Least 其实是修饰 Recently,而不应该和它并列翻译成「最近最少」,LRU 说到底就是一个时间维度上的缓存优化算法。
LRU 的兄弟 LFU
LFU(Least Frequently Used)和 LRU 比较相似,也是缓存优化算法,但是他和 LRU 唯一的区别就是,LRU 是时间维度上的,当缓存满的时候,将最久没使用的数据丢弃,而 LFU 是频率维度上的,会将最少使用(使用频次最低)的数据丢弃。
百度百科上对 LFU 的解释如下:
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
这就有点搞笑了, LFU 的翻译是「最不经常使用」,既然作为 LRU 的兄弟,难道你不应该翻译成「最经常最少使用」算法吗? 不过这好像也侧面印证了我上面的猜想应该是正确的。
同样的翻译灾难
鲁棒性
这个名词乍一听,这是什么玩意儿,完全不能从字面意思上推测出实际的含义,结果一查阅发现,这其实是英文 Robustness 的翻译,应该是直接音译的,更直观也更好听的翻译应该是「健壮性」、「稳健性」。
有理数
有人提过一个问题:有理数为什么叫「有理数」?难道是有道理的数?这个答案显然很离谱,但是我们大多数人也就得过且过吧,没有去较真了。但是细加挖掘,会发现有理数的英文是 rational number,其中 rational 被翻译成了「合理的」、「有道理的」,实际上 rational 还有一个意思是「可比的」,也就是能表示成两个整数之比的数是有理数。
这里面还有一段很有意思的历史,感兴趣的可以自行查阅。
总结
从这次经历来看,以后大家要是在一些专业名词上有困惑,不妨看看它翻译之前的英文,结合专业名称具体的含义多多推敲,如果是一个算法,就理解它的算法过程。最后会发现,一切都有源头,每个名词都有它的来历,不管是翻译错误还是什么,说不定还能了解到背后的一些历史渊源。
附:LeetCode 146. LRU 缓存实现
既然说到了 LRU 的实现,我们不妨将 LeetCode 上关于 LRU 的一道高频面试题给解决了。
题目要求如下:
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现
LRUCache类:
LRUCache(int capacity)以 正整数 作为容量capacity初始化 LRU 缓存int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。void put(int key, int value)如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value。如果插入操作导致关键字数量超过capacity,则应该 逐出 最久未使用的关键字。函数
get和put必须以O(1)的平均时间复杂度运行。
首先分析一下这道题需要的数据结构,因为获取缓存元素的 get 方法需要 O(1) 的时间复杂度,因此存储元素很容易想到使用哈希表。但是再仔细读题就会发现,LRU 的核心思想「容量不够删除最远使用的元素」蕴含了数据需要以时间为维度排列成有序序列,且需要频繁在序列头部和尾部操作,因此最终选定使用双向链表。
最终,数据结构选取「哈希表+双向链表」来实现。
首先,先定义双向链表的节点:
var ListNode = function (key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
然后,初始化 LRU 的结构:
var LRUCache = function (capacity) {
this.capacity = capacity;
this.map = new Map(); // 哈希表
// 初始化双向链表
this.head = new ListNode();
this.tail = new ListNode();
this.head.next = this.tail;
this.tail.prev = this.head;
};
由于算法中访问过的节点要移到链表末尾,因此先实现一下节点插入末尾的操作:
LRUCache.prototype.insertTail = function (node) {
this.tail.prev.next = node;
node.prev = this.tail.prev;
node.next = this.tail;
this.tail.prev = node;
}
准备工作做好之后,就开始实现最关键的 get 和 put 方法,由于题目中已经将各个情况都描述清楚了,因此按照题目要求进行逐个实现即可。
首先是 get 方法,如果缓存中没有 key,则返回 -1。如果有 key,那么将该 key 对应的节点从链表中原来的位置删除,然后插入链表末尾,代表最近访问过。最好返回该节点对应的值:
LRUCache.prototype.get = function(key) {
if (!this.map.has(key)) return -1;
let node = this.map.get(key);
// 将 node 从原来位置删除
node.prev.next = node.next;
node.next.prev = node.prev;
this.insertTail(node); // 插入链表末尾
return node.value;
};
然后是 put 方法,如果 key 已经存在,那么就把对应的节点找出来修改值,然后移到链表末尾。如果 key 不存在,要先判断一下缓存是否已满,如果满了,要先把头部节点(最远使用)删除,并在哈希表中删除记录,再插入新节点,如果没满,就直接插入新节点:
LRUCache.prototype.put = function(key, value) {
if (this.map.has(key)) {
// 如果 key 已经存在,则修改值
let node = this.map.get(key);
node.value = value;
// 移到链表末尾
node.prev.next = node.next;
node.next.prev = node.prev;
this.insertTail(node);
} else {
// 如果 key 不存在,判断是否超出容量
if (this.map.size >= this.capacity) {
let rmNode = this.head.next;
// 删除头部节点
this.head.next = rmNode.next;
rmNode.next.prev = this.head;
this.map.delete(rmNode.key);
}
// 新建节点插入链表末尾,并存入哈希表
let newNode = new ListNode(key, value);
this.map.set(key, newNode);
this.insertTail(newNode);
}
};
这道中等题在面试中出现的频率还是比较高的,因此最好还是能掌握。
LRU 居然翻译成最近最少使用?真相原来是这样!(附力扣题解)的更多相关文章
- LRU 居然翻译成最近最少使用?真相原来是这样!
前言 相信有很多同学和我一样,第一次碰到 LRU(Least Recently Used) 的这个解释「最近最少使用」都不知道是什么意思,用汤老师的话来说: 我真的感到匪夷所思啊! 最近是表示时间,最 ...
- 【探索】机器指令翻译成 JavaScript
前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...
- 机器指令翻译成 JavaScript —— No.2 跳转处理
上一篇,我们发现大多数 6502 指令都可以直接 1:1 翻译成 JS 代码,但除了「跳转指令」. 跳转指令,分无条件跳转.条件跳转.从另一个角度,也可分: 静态跳转:目标地址已知 动态跳转:目标地址 ...
- 机器指令翻译成 JavaScript —— No.3 流程分割
上一篇 我们讨论了跳转指令,并实现「正跳转」的翻译,但最终困在「负跳转」上.而且,由于线程模型的差异,我们不能 1:1 的翻译,必须对流程进行一些改造. 当初之所以选择翻译,而不是模拟,就是出于性能考 ...
- 机器指令翻译成 JavaScript —— No.4 动态跳转
上一篇,我们用模拟流程的方式,解决了跳转问题. 不过静态跳转,好歹事先是知道来龙去脉的.而动态跳转,只有运行时才知道要去哪.既然流程都是未知的,翻译从何谈起? 动态跳转,平时出现的多吗?非常多!除了 ...
- 机器指令翻译成 JavaScript —— No.5 指令变化
上一篇,我们通过内置解释器的方案,解决任意跳转的问题.同时,也提到另一个问题:如果指令发生变化,又该如何应对. 指令自改 如果指令加载到 RAM 中,那就和普通数据一样,也是可以随意修改的.然而,对应 ...
- 机器指令翻译成 JavaScript —— No.6 深度优化
第一篇 中我们曾提到,JavaScript 最终还得经过浏览器来解析.因此可以把一些优化工作,交给脚本引擎来完成. 现代浏览器的优化能力确实很强,但是,运行时的优化终归是有限的.如果能在事先实现,则可 ...
- 机器指令翻译成 JavaScript —— No.7 过渡语言
上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言.现在我们来研究一下,将 6502 指令转换成 C 的可行性. 跳转支持 翻译成 C 语言,可比 JS 容易多了.因为 C 支持 ...
- 机器指令翻译成 JavaScript —— 终极目标
上一篇,我们顺利将 6502 指令翻译成 C 代码,并演示了一个案例. 现在,我们来完成最后的目标 -- 转换成 JavaScript. 中间码输出 我们之所以选择 C,就是为了使用 LLVM.现在来 ...
- EF架构~在ef中支持IQueryable级别的Contains被翻译成了Exists,性能可以接受!
回到目录 Entityframeworks很聪明 不错,非常不错!ef里的contains比linq to sql里的contains有了明显的提升,事实上,是在进行SQL语句翻译上有所提升,在lin ...
随机推荐
- android nativate 动态注册 静态注册
说明:在java函数的入口比较容易分析, 把activity的生命周期或者关键函数通过放在so层,分析起来就困难多了 1.在MainActivity中 package com.demo.nativat ...
- Day37:正则表达式详解
正则表达式 1.1 概述 正则表达式可以用一些规定的字符来制定规则,并用来校验数据格式的合法性. 比如我们在网站上输入用户账号,要求我们输入的账号信息要符合账号的格式,而校验我们输入的账号格式是否正确 ...
- 个人电脑公网IPv6配置
一.前言 自己当时以低价买的阿里ECS云服务器马上要过期了,对于搭建个人博客.NAS这样服务器的需求购买ECS服务器成本太高了,刚好家里有台小型的桌面式笔记本,考虑用作服务器,但是公网IPv4的地址实 ...
- 【深入浅出Spring原理及实战】「源码原理实战」从底层角度去分析研究PropertySourcesPlaceholderConfigurer的原理及实战注入机制
Spring提供配置解析功能 主要有一下xml文件占位符解析和Java的属性@Value的占位符解析配置这两种场景进行分析和实现解析,如下面两种案例. xml文件的占位符解析配置 <bean i ...
- overflow:scroll修改样式
当overflow :scroll 出现滚动条后,默认的滚动条样式太丑了,不是我们想要的,那么我们来修改一下吧!~ 话不多说,直接上代码 /* 定义滚动条样式 */ ::-webkit-scroll ...
- 正确理解和使用JAVA中的字符串常量池
前言 研究表明,Java堆中对象占据最大比重的就是字符串对象,所以弄清楚字符串知识很重要,本文主要重点聊聊字符串常量池.Java中的字符串常量池是Java堆中的一块特殊存储区域,用于存储字符串.它的实 ...
- 【Java技术专题】「原理专题」深入分析Java中finalize方法的作用和底层原理
finalize方法是什么 finalize方法是Object的protected方法,Object的子类们可以覆盖该方法以实现资源清理工作,GC在首次回收对象之前调用该方法. finalize方法与 ...
- TiDB上百T数据拆分实践
背景 提高TiDB可用性,需要把多点已有上百T TiDB集群拆分出2套 挑战 1.现有需要拆分的12套TiDB集群的版本多(4.0.9.5.1.1.5.1.2都有),每个版本拆分方法存在不一样 2.其 ...
- Spark详解(02) - Spark概述
Spark详解(02) - Spark概述 什么是Spark Hadoop主要解决,海量数据的存储和海量数据的分析计算. Spark是一种基于内存的快速.通用.可扩展的大数据分析计算引擎. Hadoo ...
- python数据分析与可视化【思维导图】
python数据分析与可视化常用库 numpy+matplotlib+pandas 思维导图 图中难免有错误,后期随着学习与应用的深入,会不断修改更新. 当前版本号:1.0 numpy介绍 NumPy ...