带你学会 LRU 算法相关内容
Posted 南淮北安
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你学会 LRU 算法相关内容相关的知识,希望对你有一定的参考价值。
文章目录
一、什么是 LRU
就是一种缓存淘汰策略。
计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。
但问题是,删除哪些内容呢?我们肯定希望删掉哪些没什么用的缓存,而把有用的数据继续留在缓存里,方便之后继续使用。
那么,什么样的数据,我们判定为「有用的」的数据呢?
LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是是有用的,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据
当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景
二、LRU 算法描述
LRU 算法实际上是让你设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。
注意哦,get 和 put 方法必须都是 O ( 1 ) O(1) O(1) 的时间复杂度,我们举个具体例子来看看 LRU 算法怎么工作。
/* 缓存容量为 2 */
LRUCache cache =newLRUCache(2);
// 你可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 最近使用的排在队头,久未使用的排在队尾
// 圆括号表示键值对 (key, val)
cache.put(1,1);
// cache = [(1, 1)]
cache.put(2,2);
// cache = [(2, 2), (1, 1)]
cache.get(1);// 返回 1
// cache = [(1, 1), (2, 2)]
// 解释:因为最近访问了键 1,所以提前至队头
// 返回键 1 对应的值 1
cache.put(3,3);
// cache = [(3, 3), (1, 1)]
// 解释:缓存容量已满,需要删除内容空出位置
// 优先删除久未使用的数据,也就是队尾的数据
// 然后把新的数据插入队头
cache.get(2);// 返回 -1 (未找到)
// cache = [(3, 3), (1, 1)]
// 解释:cache 中不存在键为 2 的数据
cache.put(1,4);
// cache = [(1, 4), (3, 3)]
// 解释:键 1 已存在,把原始值 1 覆盖为 4
// 不要忘了也要将键值对提前到队头
三、LRU 算法设计
要让 put 和 get 方法的时间复杂度为 O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分
因为显然 cache 必须有顺序之分,以区分最近使用的和久未使用的数据;而且我们要在 cache 中查找键是否已存在;如果容量满了要删除最后一个数据;每次访问还要把数据插入到队头。
我们已经知道 哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。这个数据结构长这样:
整体的设计思路是,使用 HashMap 存储 key,这样可以做到 put 方法和 get 方法的时间复杂度都是 O(1)
,而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点
之所以选择双向链表而不选用单链表,是因为删除操作时,需要借助前驱节点,双向链表支持直接查找前驱的操作
LRU 存储是基于双向链表实现的,其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)
的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。
put 方法:首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果 LRU 空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
get 方法:通过 HashMap 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。
四、Java 代码实现
// 定义双链表节点
class Node
int key,val;
Node preNode,nextNode;
Node()
Node(int key,int val)
this.key = key;
this.val = val;
// 定义双链表
class DoubleList
Node head,tail;
int size;
// 头部增加元素
public void addFirst(Node node)
if(head==null)
head = tail = node;
else
node.nextNode = head;
head.preNode = node;
head = node;
size++;
// 移出节点node
public void remove(Node node)
if(head==node && tail==node)
tail = null;
head = null;
else if(tail==node)
node.preNode.nextNode = null;
tail = tail.preNode;
else if(head==node)
node.nextNode.preNode = null;
head = head.nextNode;
else
node.nextNode.preNode = node.preNode;
node.preNode.nextNode = node.nextNode;
size--;
// 移出最后一个节点
public Node removeLast()
Node node = tail;
remove(tail);
return node;
public int size()
return size;
class LRUCache
Map<Integer,Node> map;
DoubleList cache;
int capacity;
public LRUCache(int capacity)
map = new HashMap<>();
cache = new DoubleList();
this.capacity = capacity;
// 获取元素
public int get(int key)
// 不存在返回 -1
if(!map.containsKey(key))
return -1;
// 存在更新该节点的位置然后返回值
Node node = map.get(key);
put(key,node.val);
return node.val;
// 存储节点
public void put(int key, int value)
Node node = new Node(key,value);
// 如果已经存在,则更新即可
if(map.containsKey(key))
// 移出原始的元素
cache.remove(map.get(key));
// 将其更新为首位
cache.addFirst(node);
map.put(key,node);
else
// 添加时,判断是否达到容量上限
if(cache.size()==capacity)
Node lastNode = cache.removeLast();
map.remove(lastNode.key);
cache.addFirst(node);
map.put(key,node);
五、Redis 中 LRU 的实现
如果按照HashMap和双向链表实现,需要额外的存储存放 next 和 prev 指针,牺牲比较大的存储空间,显然是不划算的。
所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的
六、线程安全的LRU算法
public class LRUCache extends LinkedHashMap<String,String>
private int cache_size;
public LRUCache(int capacity)
super(capacity, 0.75f, true);
this.cache_size = capacity;
public String get(String key)
synchronized (LRUCache.class)
return super.getOrDefault(key, "");
public String put(String key, String value)
synchronized (LRUCache.class)
return super.put(key, value);
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest)
return this.size() > cache_size;
单例模式:
public class LRUCache extends LinkedHashMap<String, String>
private int cache_size;
private static LRUCache instance;
private LRUCache(int capacity)
super(capacity, 0.75f, true);
this.cache_size = capacity;
public static synchronized LRUCache getInstance()
if (instance == null)
instance = new LRUCache(100);
return instance;
public String get(String key)
synchronized (this)
return super.getOrDefault(key, "");
public String put(String key, String value)
synchronized (this)
return super.put(key, value);
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest)
return this.size() > cache_size;
或者把Map改为CurrentHashMap<>();
以上是关于带你学会 LRU 算法相关内容的主要内容,如果未能解决你的问题,请参考以下文章