什么是 LRU
LRU(Least Recently Used)即最近最少使用缓存,
在做性能优化的时候会经常用到使用到缓存,用以空间换时间的方式来达到性能优化目标。
最常用到的缓存库就是 lru-cache, 前端 SSR 框架 nuxt 就是使用 lru-cache 来实现其页面缓存、组件缓存和接口缓存的功能,以降低服务器 cpu 的负载提高 SSR 页面的并发数。
LRU 实现
先来看看 LRU 需要达到什么样的要求。
- 需要给定一个数据结构的长度,不能无限制的缓存数据;
- LRU 实例提供一个 get 方法,可通过关键字 key 获取缓存中数据,若没有则返回 -1;
- LRU 实例提供一个 put 方法,变更数据值,若数据存在则修改,不存在则插入一条新数据,插入时超过数据长度则删除最久未使用的关键字。
- get、put的时间复杂度必须是 O(1)
基础数据结构选型
问题一
对于一个优秀的前端工程师来说,想要实现 1、2、3 的要求是很容易的,用纯数组就可以实现上述 3 点的需求,但是在时间复杂度的要求上达不到,用数组的话,不管是 get 还是 put 方法的时间复杂度都为 O(n)
这里就不详细展开数组的实现了, 为什么时间复杂度为 O(n) ???
所以为了实现对存储结构操作的,这里选择双向链表来时现缓存数据的存储。问题二
在选择双向链表后又出现了一个新问题, 众所周知,对链表插入与删除时间复杂度为 O(1), 但是链表的查找时间复杂度却为 O(n),在实现 get 的过程中肯定会使用到查询操作。为了解决查询的时间复杂度问题,自然就想到了 Map 数据结构。es6 的 Map 数据结构是查询的时间复杂度正是为O(1)。
这里肯定又有小朋友问啦:用 Map 为什么不直接用对象呢,这多方便?
其实这里使用对象也是可以实现一些简单的需求的,如果 key 为 number 或者为 string,只需要操作时转化一下即可,也能实现需求。但是需要 key 类型更复杂的场景,直接使用对象时达不到 LRU 的要求的
这里通过 双向链表,Map 的数据结构的组合来实现 LRU,使用双向链表存储数据,使用Map标记链表中 key 的位置这样就可以很容易实现 LRUCache 的数据结构。
因为要实现 O(1) 的时间复杂度,首先想到的就是使用 es6 的 Map 数据结构来实现。
/*** @param {number} capacity*/var LRUCache = function(capacity) {this.map = new Map();this.capacity = capacity;};/*** @param {number} key* @return {number}*/LRUCache.prototype.get = function(key) {if(this.map.has(key)){let value = this.map.get(key);this.map.delete(key); // 删除后,再 set ,相当于更新到 map 最后一位this.map.set(key, value);return value} else {return -1}};/*** @param {number} key* @param {number} value* @return {void}*/LRUCache.prototype.put = function(key, value) {// 如果已有,那就要更新,即要先删了再进行后面的 setif(this.map.has(key)){this.map.delete(key);}this.map.set(key, value);// put 后判断是否超载if(this.map.size > this.capacity){this.map.delete(this.map.keys().next().value);}};/*** Your LRUCache object will be instantiated and called as such:* var obj = new LRUCache(capacity)* var param_1 = obj.get(key)* obj.put(key,value)*/
简单用 Typescript 实现一下
class LinkNode {key: numbervalue: number_prev: LinkNode | null_next: LinkNode | nullconstructor(key: number, val: number) {this.key = keythis.value = valthis._prev = nullthis._next = null}}class LRUCache {head: LinkNode | nulltail: LinkNode | nullsize: numbermap: Map<number, LinkNode>constructor(capacity: number) {this.size = capacitythis.map = new Map()this.head = nullthis.tail = null}get(key: number): number {// 在 map 中查找是否存在有这条数据if (this.map.has(key)) {let node = this.map.get(key) as LinkNodelet _prev = node._prevlet _next = node._next// 判断是否为头节点,若为头节点则不需要对链表作任何操作,直接返回值,若部位头肩点,需要操作链表达到最近缓存的操作if (_prev) {// 不为节点需要将该节点移动到头节点的位置// 当前节点是尾部节点的情况if (!_next) {this.tail = _prev} else {_next._prev = _prev // 等价于 node._next._prev = node._prev}// 1. 链表操作,移除 node 节点_prev._next = _next // 等价于 node._prev._next = node._next// 2. 将 node 节点放到链表头部node._prev = nullif (this.head) {node._next = this.headthis.head._prev = node}this.head = node}// 返回想要查找的数据值return node.value}return -1}put(key: number, value: number): void {// put 功能包括两部分,修改和新增if (this.map.has(key)) {// 修改let node = this.map.get(key) as LinkNodenode.value = valuelet _prev = node._prevlet _next = node._nextif (_prev) {if (!_next) {this.tail = _prev} else {_next._prev = _prev // 等价于 node._next._prev = node._prev}// 非头部节需要把节点提到链表头部_prev._next = _nextnode._next = this.headnode._prev = nullthis.head && (this.head._prev = node)this.head = node}} else {let node = new LinkNode(key, value)this.map.set(key, node)if (this.head) {node._next = this.headthis.head._prev = nodethis.head = node} else {this.head = this.tail = node}// 新增数据的场景if (this.map.size > this.size) {let _tail = this.tail as LinkNodethis.map.delete(_tail.key)this.tail = _tail._previf (this.tail) {this.tail._next = null}if (this.head.key === _tail.key) {this.head = null}}}}}
