LRU原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
1. 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
分析
【命中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。
package com.yifan.permissions
import java.util.concurrent.locks.ReentrantLock
/**
* 类说明:利用LinkedHashMap实现简单的缓存,
* 必须实现removeEldestEntry方法,具体参见JDK文档
*/
class LRULinkedHashMap : LinkedHashMap {
//最大容量
private val maxCapacity: Int
private val lock = ReentrantLock()
companion object {
const val DEFAULT_LOAD_FACTOR: Float = 0.75f
}
constructor(maxCapacity: Int) : super(maxCapacity, DEFAULT_LOAD_FACTOR, true) {
this.maxCapacity = maxCapacity;
}
override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
return size > maxCapacity
}
override fun containsKey(key: K): Boolean {
try {
lock.lock();
return super.containsKey(key)
} finally {
lock.unlock()
}
}
override fun get(key: K): V? {
try {
lock.lock()
return super.get(key)
} finally {
lock.unlock()
}
}
override fun put(key: K, value: V): V? {
try {
lock.lock()
return super.put(key, value)
} finally {
lock.unlock()
}
}
override val size: Int
get() = try {
lock.lock()
super.size
} finally {
lock.unlock()
}
override fun clear() {
try {
lock.lock()
super.clear()
} finally {
lock.unlock()
}
}
fun getAll(): Collection> {
try {
lock.lock();
return ArrayList>(super.entries);
} finally {
lock.unlock();
}
}
}
LRUCache的链表+HashMap实现
传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。
它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。
它的原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。
这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。
当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。
上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。
非线程安全,若实现安全,则在相应的方法加锁。
package com.yifan.permissions
//链表+HashMap实现LRU
class LRUCache {
// private var currentCacheSize: Int = 0
private var cacheCapacity: Int = 0 //容量
private var caches: HashMap //便于查询对象是否存在,效率更高
private var first: CacheNode? = null //头部
private var last: CacheNode? = null //尾部
constructor(size: Int){
this.cacheCapacity = size
caches = HashMap(size)
}
fun put(k: K, v: V){
var node = caches[k]
if(node == null){
//超过容量,移除尾部节点
if(caches.size >= cacheCapacity){
caches.remove(last?.key)
removeLast()
}
//新建节点
node = CacheNode()
node.key = k
}
node.value = v
//最新使用的节点移动到头部
moveToFirst(node)
caches[k] = node
}
fun get(k: K): V? {
var node = caches.get(k)
return when(node){
null -> null
else->{
//最新使用的节点移动到头部
moveToFirst(node);
node.value
}
}
}
//移除节点
fun remove(k: K): CacheNode? {
var node = caches[k]
if(node != null){
if(node.pre != null){
node.pre?.next = node.next
}
if(node.next != null){
node.next?.pre = node.pre
}
if(node == first){
first = node.next;
}
if(node == last){
last = node.pre;
}
}
return caches.remove(k)
}
//移动到头部
private fun moveToFirst(node: CacheNode){
if(first == node){
return;
}
if(node.next!= null){
node.next?.pre = node.pre
}
if(node.pre != null){
node.pre?.next = node.next
}
if(node == last){
last = last?.pre
}
if(first == null || last == null){
first = node
last = node
return;
}
node.next=first
first?.pre = node
first = node
first?.pre=null
}
private fun removeLast(){
if(last != null){
last = last?.pre
if(last == null){
first = null
}else{
last?.next = null
}
}
}
fun clear(): Unit{
first = null
last = null
caches.clear()
}
inner class CacheNode{
var pre : CacheNode? = null
var next: CacheNode? = null
var key: K? = null
var value: V? = null
}
override fun toString(): String{
var sb = StringBuilder();
var node = first;
while(node != null){
sb.append(String.format("%s:%s ", node.key,node.value));
node = node.next;
}
return sb.toString();
}
}
fun main(args: Array) {
var lru = LRUCache(3)
lru.put(1, "a"); // 1:a
println(lru.toString());
lru.put(2, "b"); // 2:b 1:a
println(lru.toString());
lru.put(3, "c"); // 3:c 2:b 1:a
println(lru.toString());
lru.put(4, "d"); // 4:d 3:c 2:b
println(lru.toString());
lru.put(1, "aa"); // 1:aa 4:d 3:c
println(lru.toString());
lru.put(2, "bb"); // 2:bb 1:aa 4:d
println(lru)
}
输出日志如下:
1:a
2:b 1:a
3:c 2:b 1:a
4:d 3:c 2:b
1:aa 4:d 3:c
2:bb 1:aa 4:d
设计LRU缓存结构
设计LRU(最近最少使用)缓存结构,该结构在构造时确定大小,假设大小为 k ,并有如下两个功能
1. set(key, value):将记录(key, value)插入该结构
2. get(key):返回key对应的value值
提示:
1.某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的,然后都会刷新缓存。
2.当缓存的大小超过k时,移除最不经常使用的记录。
3.输入一个二维数组与k,二维数组每一维有2个或者3个数字,第1个数字为opt,第2,3个数字为key,value
若opt=1,接下来两个整数key, value,表示set(key, value)
若opt=2,接下来一个整数key,表示get(key),若key未出现过或已被移除,则返回-1
对于每个opt=2,输出一个答案
4.为了方便区分缓存里key与value,下面说明的缓存里key用""号包裹
要求:set和get操作复杂度均为 O(1)O(1)
输入:
[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
复制返回值:
[1,-1]
复制说明:
[1,1,1],第一个1表示opt=1,要set(1,1),即将(1,1)插入缓存,缓存是{"1"=1} [1,2,2],第一个1表示opt=1,要set(2,2),即将(2,2)插入缓存,缓存是{"1"=1,"2"=2} [1,3,2],第一个1表示opt=1,要set(3,2),即将(3,2)插入缓存,缓存是{"1"=1,"2"=2,"3"=2} [2,1],第一个2表示opt=2,要get(1),返回是[1],因为get(1)操作,缓存更新,缓存是{"2"=2,"3"=2,"1"=1} [1,4,4],第一个1表示opt=1,要set(4,4),即将(4,4)插入缓存,但是缓存已经达到最大容量3,移除最不经常使用的{"2"=2},插入{"4"=4},缓存是{"3"=2,"1"=1,"4"=4} [2,2],第一个2表示opt=2,要get(2),查找不到,返回是[1,-1]
输入:
[[1,1,1],[1,2,2],[2,1],[1,3,3],[2,2],[1,4,4],[2,1],[2,3],[2,4]],2
复制返回值:
[1,-1,-1,3,4] 操作类型set时只负责处理LRU操作; 操作类型get时,判断是否已存在LRU链表中,存在list添加value,不存在则添加-1;
import java.util.*;
public class Solution {
/**
* lru design
* @param operators int整型二维数组 the ops
* @param k int整型 the k
* @return int整型一维数组
*/
public int[] LRU (int[][] operators, int k) {
// write code here
Map map = new LinkedHashMap<>();
ArrayList list = new ArrayList();
for(int[] operator : operators){
int key = operator[1];
switch(operator[0]){
case 1:
if(map.size()
参考:
缓存淘汰算法--LRU算法(java代码实现)_long的博客-CSDN博客_lru算法java实现
设计LRU缓存结构_牛客题霸_牛客网