LRU缓存淘汰算法

 来源公众号:算法面试题

LRU缓存淘汰算法

简介

 

LRU,最近最少使用算法,即当要对数据进行淘汰时,先将最近最久未使用的数据淘汰(亦说最近最久未使用算法),是一种缓存淘汰算法。

 

LRU缓存淘汰算法

 

意义:大家知道计算机的内存是有限的,当我们对一些数据一直缓存而不淘汰的话,那么内存总会被占满的,特别是服务器,造成的后果是可怕的。

 

应用场景:内存的页面淘汰、站内搜索关键字的缓存淘汰(详见:《如何应对算法面试题》 来源公众号:算法面试题)等。

 

题目

 

(猫眼面试题,来源于剑指offer)

运用你所掌握的数据结构,设计和实现一个LRU缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put

 

 

获取数据 get(key) - 如果** (key) 存在于缓存中,则获取**的值(总是正数),否则返回 -1。


写入数据 put(key, value) - 如果**不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

 

 

要求:

 

请对算法进行优化,在 O(1) 时间复杂度内完成这两种操作

 

 

示例:

 

LRUCache cache = 
new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);   //返回  1
cache.put(3, 3);//该操作会使得
                //**2被淘汰
cache.get(2);   //返回 -1 (未找到)
cache.put(4, 4);//该操作会使得
                //**1被淘汰
cache.get(1);   //返回 -1 (未找到)
cache.get(3);   //返回  3
cache.get(4);   //返回  4

思考三分钟再往下看效果更佳哦

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

思路

 

本题属于一道比较难的题,难点在于O(1)的时间复杂度,首先我们会想到用链表来缓存,为了方便淘汰最后一条数据时将尾指针前移一位,我们需要使用双链表,于是我们定义结点如下。

 

LRU缓存淘汰算法

 

接下来进行get(key)操作时对整个链表进行遍历(时间复杂度O(n),n为缓存容量),直到找到key结点返回对应的值(找不到返回-1),注:若结点存在,访问完结点后还应将结点移到链表首部。

 

接下来是put(key,val)操作,首先需要对链表遍历(时间复杂度O(n),n为缓存容量)以确定key结点是否已经存在,若存在将结点移到链表首部并更新结点值为val;若不存在,则需要新建一个结点(key,val),并插入首部,需注意的是若超过容量上限,则删除最后一个结点。

 

不难发现,上面的实现方法get与put时间复杂度均为O(n),时间主要浪费在了遍历链表(查找key结点)上,那么我们如何来优化查找key结点的效率呢?

 

谈到查找并且是O(1)复杂度,那么我们不难想到哈希表。结合本题我们可以创建一个这样的Map,Map结点的键为双链表结点的key,Map结点的值为指向双链表结点的指针,这样我们通过链表结点的key就可以在O(1)时间内找到对应结点了。需要注意的是在对双链表增加或淘汰结点时,也要对Map同步添加或删除结点。

 

代码实现

 

下面是作者用JavaScript实现的代码,仅供参考!(建议大家自己动手实现一遍)

function Node(key, val) {
	//定义双链表的结点
	this.key = key;
	this.val = val;
	this.pre = this.next = null;
}

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
	this.capacity = capacity;
	//定义头结点,不存值,作用方便结点的插入和删除
	//双链表,head始终指向头结点,tail始终指向最后一个结点
	this.tail = this.head = new Node(-2, -2);
	//map,键是结点的属性key,值是指向结点的指针
	//map作用保证了O(1)时间内实现get与put操作
	this.map = new Map();
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
	let node = this.map.get(key);
	//当key值不存在或已被淘汰时,返回-1
	if(node === undefined) return -1;
	//当key值存在于链表中,返回值并将对应结点移到链表首部
	//注:链表具有头结点,故移到首部,实际上是移到头结点之后
	node.pre.next = node.next;
	if(this.tail != node) {
		node.next.pre = node.pre;
	} else {
		this.tail = this.tail.pre;
	}
	node.pre = this.head;
	node.next = this.head.next;
	if(node.next != null) node.next.pre = node;
	else this.tail = node;
	this.head.next = node;
	return node.val;
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
	let node = this.map.get(key);
	if(node !== undefined) {
		//如果key存在于双链表中,更新值并将key对应的那个结点移到链表首部
		//注:链表具有头结点,故移到首部,实际上是移到头结点之后
		node.val = value;
		node.pre.next = node.next;
		if(this.tail != node) {
			node.next.pre = node.pre;
		} else {
			this.tail = this.tail.pre;
		}
		node.pre = this.head;
		node.next = this.head.next;
		if(node.next != null) node.next.pre = node;
		else this.tail = node;
		this.head.next = node;
	} else {
		//如果key不在双链表中,创建一个新结点,并将结点插入链表首部
		//注:链表具有头结点,故插入首部,实际上是插入到头结点之后
		node = new Node(key, value);
		if(this.map.size == 0) {
			this.tail = node;
			node.pre = this.head;
			node.next = this.head.next;
			this.head.next = node;
		} else {
			node.pre = this.head;
			node.next = this.head.next;
			node.next.pre = node;
			this.head.next = node;
		}
		if(this.map.size >= this.capacity) {
			//如果缓存容量达到上限,需要将最近最久未使用的那条数据淘汰
			//即淘汰最后一条数据
			this.map.delete(this.tail.key);
			this.tail = this.tail.pre;
			this.tail.next = null;
		}
		//将新结点的key注册到map中,方便在O(1)的时间内获取到值
		this.map.set(key, node);
	}
};

 

完毕,小伙伴们可以愉快地去敲键盘实现自己的代码了!