这个算法非常经典,手动记录一下。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。 该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t,当须淘汰一个页面时,选择现有页面中其t 值最大的,即最近最少使用的页面予以淘汰。---wiki
实现 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) 的平均时间复杂度运行。---leetcode 146
经典双向链表
class LRUCache {
constructor(capacity){
this.capacity = capacity
this.cacheCount = 0
// 【先】用对象来使 get 操作时间复杂度为O(1)
this.cache = {}
// *关键点
// 这里使用两个对象做指针 头指针永远指向第一个节点 尾指针指向最后一个节点
// 如果有新加入的节点则从头指针入手改变引用关系
// 如果超过缓存空间则从尾巴指针获取最后一个节点从cache中删除并且改变引用关系
// 即用双向链接结构来解决普通对象无序的问题
this.dummy = {
prev: null,
val: undefined,
next: null
}
this.tail = {
prev: this.dummy,
val: undefined,
next:null
}
this.dummy.next = this.tail // 初始化时cache中没有任何数据因此这两个指针互相指向对方
}
get(key) {
const node = this.cache[key]
if(!node) return -1
this.appendToHead(key) // 当对某一个节点进行get操作时将它移动到链的头部
return node.val
}
put(key,value) {
// 判断当前put的key是否已经在缓存中 如果是则直接更新并将它移动到链表头部
if(this.cache[key]) {
this.cache[key].val = value
this.appendToHead(key)
return
}
// 判断容量是否已满如果已经满了那就先把最后一个节点删掉
if(this.cacheCount === this.capacity) this.deleteLast()
// 获取原本最靠前的node
const initialHead = this.dummy.next
const newNode = {
val: value,
next: initialHead,
prev: this.dummy,
key // node中记录key来方便我们在cache中获取
}
this.dummy.next = newNode
initialHead.prev = newNode
this.cache[key] = newNode
this.cacheCount++
}
deleteLast() {
const node = this.tail.prev // tail指针永远指向最后一个node
const initialPrev = node.prev // 获取将要删除的节点的上一个节点
this.tail.prev = initialPrev
initialPrev.next = this.tail
delete this.cache[node.key] // 根据key从cache中清除缓存节点
this.cacheCount--
}
appendToHead(key) {
const node = this.cache[key]
// 如果前一个节点是dummy节点说明已经是第一个节点了 就不用动了
// 这在处理只有一个节点的时候比较有用
if(node.prev.val === undefined) return
// 在dummy指针和原节点之间插入这个node
// 这里需要注意的一点是除了维护新的关系,原节点自身上下节点的关系也需要维护
let initialNext = this.dummy.next // 记录原本的头节点
this.dummy.next = node // 改写头节点的指向
const nodePrev = node.prev // 记录该节点原本的前一个节点
const nodeNext = node.next // 记录该节点原本的下一个节点
// 维护该节点和dummy以及原本dummy指向的节点之间的关系
node.prev = this.dummy
initialNext.prev = node
node.next = initialNext
// 维护该节点原始前一个节点和后一个节点的关系
nodeNext.prev = nodePrev
nodePrev.next = nodeNext
}
}
使用Map数据结构
在上面部分我们留下了一个伏笔:
是不是只有有这么一种数据结构既可以做到 普通对象O(1)的读写也可以记录更新顺序呢?
答案是有的,那就是ES6的 Map 【MDN】
其主要原理就是依靠了 Map数据结构能够记录键值对被添加的顺序
class LRUCache {
constructor(props) {
const { capacity = 1 } = props
this.capacity = capacity
this.cache = new Map()
}
get(key) {
let val = this.cache.get(key)
if(val === undefined) return -1
this.cache.delete(key)
this.cache.set(key, val) // 重新set 就能使对应的key的顺序更新
return val
}
put(key,val) {
if(this.cache.has(key)) this.cache.delete(key)
this.cache.set(key,value)
// 通过map对象的size静态属性可以获取当前map的容量
if(this.cache.size > this.capacity){
// map.keys方法返回一个可迭代的对象
let keyIterator = this.cache.keys();
// 获取最后一个键名
this.cache.delete(keyIterator.next().value);
}
}
}
相信通过这个示例可以加深不少对于 Map的理解