前言

LRU是一个操作系统的页面置换算法,它是 Least Recently Used(最近最少使用) 的缩写,用来淘汰上次使用距当前时间最长的页,可以理解为当缓存满了后操作系统实现的一个置换缓存的算法。在​​vue​​​的​​keep-alive​​组件中也使用到了LRU算法。常见的页面置换算法有FIFO(先进先出)、clock(时钟)等。

下面模拟LRU算法给出页面置换的例子:手撕代码系列:LRU算法_缓存手撕代码系列:LRU算法_链表_02

  1. 如果当前缓存没有满,那么向后面插入即可。
  2. 第五步的时候以为在缓存中无法找到页地址流为5的页地址,并且缓存已经满了,所以需要选择一个合适的页来置换插入的页。按照最近最少使用的顺序来排序,页序号依次为3、2、1,所以淘汰的页为3。
  3. 后面的操作和前面的一致,都是在缓存中寻找也是否存在,如果不存在则进行页面置换。
    下面就来实现一个页面置换算法。

实现思路

我们的目的就是为了找出最近最少使用的那一部分并将它替换,一个比较粗暴的方法就是使用时间戳来标记然后比较各个内存块的时间戳,但是这样子要花费**O(n)**的时间来比较时间戳的大小,一旦数据量多了起来比较时间将会直线上升。

在对算法进行优化时使用的较多的就是以空间换时间,在这里我们使用双链表+Map来实现LRU算法。数据结构设计如下:手撕代码系列:LRU算法_链表_03具体的算法流程图如下:手撕代码系列:LRU算法_页面置换_04

代码实现

  1. 节点的实现

使用一个类的实例来代表一个节点,节点的数据有:

  • 当前key
  • 当前value
  • 头指针
  • 尾指针
class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}
  1. LRU的实现
class LRU_TAG {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
this.head = new Node(null, null);
this.tail = new Node(null, null);
this.head.next = this.tail;
this.tail.prev = this.head;
}

put(key, value) {}

get(key) {}
}

首先构造函数传入一个capacity作为这个链表的大小,然后使用两个哨兵分别代表链表头和链表尾,这样子就只考虑一般情况就行了。

  1. put函数的实现
put(key, value) {
const node = this.map.get(key);
//如果对应节点已经存在
if (node) {
node.value = value;
//将该节点移动到链表头
this.removeAndInsert(node);
} else {
const temp = new Node(key, value);
//链表是否已满
if (this.map.size >= this.capacity) {
this.map.delete(this.tail.prev.key);
this.tail.prev = this.tail.prev.prev;
this.tail.prev.next = this.tail;
}

this.map.set(key, temp);
temp.next = this.head.next;
this.head.next = temp;
temp.prev = this.head;
temp.next.prev = temp;
}
}

首次查找map中是否存在对应的key,然后确认链表是否已满,如果链表已满,那么就要删除链表的尾节点,然后在链表头插入新节点。

  1. get函数的实现
get(key) {
const node = this.map.get(key);
if (node) {
//将该节点移动到链表头
this.removeAndInsert(node);
return node.value;
}
return null;
}
  1. removeAndInsert函数的实现
removeAndInsert(node) {
if (node === this.head.next) return;
node.prev.next = node.next;
node.next.prev = node.prev;

node.next = this.head.next;
node.next.prev = node;
node.prev = this.head;
this.head.next = node;
}

完整代码

class LRU_TAG {
constructor(capacity) {
this.capacity = capacity >= 1 ? capacity : 1;
this.map = new Map();
this.head = new Node(null, null);
this.tail = new Node(null, null);
this.head.next = this.tail;
this.tail.prev = this.head;
}

put(key, value) {
const node = this.map.get(key);
//如果对应节点已经存在
if (node) {
node.value = value;
//将该节点移动到链表头
this.removeAndInsert(node);
} else {
const temp = new Node(key, value);
//链表是否已满
if (this.map.size >= this.capacity) {
this.map.delete(this.tail.prev.key);
this.tail.prev = this.tail.prev.prev;
this.tail.prev.next = this.tail;
}

this.map.set(key, temp);
temp.next = this.head.next;
this.head.next = temp;
temp.prev = this.head;
temp.next.prev = temp;
}
}

get(key) {
const node = this.map.get(key);
if (node) {
//将该节点移动到链表头
this.removeAndInsert(node);
return node.value;
}
return null;
}

/**
*
* @param node {Node}
*/
removeAndInsert(node) {
if (node === this.head.next) return;
node.prev.next = node.next;
node.next.prev = node.prev;

node.next = this.head.next;
node.next.prev = node;
node.prev = this.head;
this.head.next = node;
}
}

class Node {
constructor(key, value) {
this.key = key;
this.value = value;
this.prev = null;
this.next = null;
}
}

总结

使用以上的LRU算法代码能够在O(1)的时间查找和删除节点,LRU的优势在于对热点数据的维护,但是LRU对批量数据的适配比较差,比如有一批数据频繁地插入就会导致链表频繁地换入换出,造成缓存污染;