LRU算法介绍
LRU算法是一种数据淘汰策略,英文叫做Least Recently Used,,意思就是在数据空间有限的情况下,我们要保留最近访问的数据,一旦超过容量,就要将最近最少使用的数据淘汰。可以理解为在最近一段时间里,缓存元素按照访问的时间戳进行排序,比如时间戳最大的排序越靠前(也可能越靠后),比如元素A的访问时间为00:21,元素B的访问时间为00:31,那么排序时元素B就比元素A的排序更靠前。
介绍完LRU,再介绍下LFU算法以做区分,LFU是最近最不常用,可以理解为在最近一段时间内,元素按照访问次数(频率)排序,访问频率越多排序越靠前。
LRU和LFU的区别是LRU是时间维度的,按照访问时间排序;LFU是次数维度,按照访问次数排序。
在往下看LRU如何实现之前,我们可以先试想下如果让你来实现LRU你会如何实现。可以从下面几个角度开始思考:
- LRU算法是用于缓存淘汰的,缓存都是key-value键值对,所以在实现LRU时构造函数需要能接收key-value参数
- 参考redis,对缓存键值对操作无非以下两种:Put操作和Get操作,这是需要实现的方法,Put操作是当数据存在时进行更新,不存在时则添加
- 每次访问数据都需要对key进行排序,将刚访问过的数据排序到前面,这是LRU实现最重要的步骤
- key用什么数据结构,value用什么数据结构
用LinkeHashMap实现
LinkeHashMap就是哈希链表,是双向链表和哈希表的结合,哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢,而哈希链表兼具了哈希表和链表的优势:能快速查找,插入删除快,而且元素有序
LinkeHashMap具有两个属性:
-
顺序插入
LinkedHashMap默认按照插入顺序迭代元素。支持遍历时会按照插入顺序有序进行迭代。 默认构造函数构造出来的LinkeHashMap就是按照插入顺序排序,比如:HashMap < String, String > map = new LinkedHashMap < > (); -
访问有序
LinkedHashMap可以按照元素访问顺序排序,这点可以被用于LRU算法。 那么怎么让LinkedHashMap按照元素访问顺序排序呢?可以看下它提供的一个构造函数
/**
* Constructs an empty {@code LinkedHashMap} instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - {@code true} for
* access-order, {@code false} for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
LinkedHashMap 定义了排序模式 accessOrder(boolean 类型,默认为 false),访问顺序则为 true,插入顺序则为 false。
为了实现访问顺序遍历,我们可以使用传入 accessOrder 属性的 LinkedHashMap 构造方法,并将 accessOrder 设置为 true,表示其具备访问有序性,比如:
LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);
LinkedHashMap怎么实现访问排序:
访问一个元素这个概念不够具体,更详细的定义是插入一个新元素或者访问一个已有元素都属于访问了一个元素。LinkedHashMap是一个链表,当插入一个新元素时,会追加在链表表尾;当访问一个已有元素时,会将这个元素取出放在链表尾部。而LinkedHashMap迭代元素都是从表头遍历到表尾,这样就实现了访问排序,最近使用的排在链表尾,久未使用的排在链表头。
代码
class LRUCache {
LinkedHashMap<Integer,Integer> map;
int capacity;
public LRUCache(int capacity) {
map = new LinkedHashMap<>(capacity,0.75f,true);
this.capacity = capacity;
}
public int get(int key) {
return map.getOrDefault(key,-1);
}
public void put(int key, int value) {
if (map.containsKey(key)){
map.put(key,value);
return;
}else {
map.put(key,value);
if (map.size() > capacity){
//将表头元素移除
int firstKey = map.keySet().iterator().next();
map.remove(firstKey);
}
return;
}
}
}
使用哈希表+双向链表实现LRU算法
要做到查询时间复杂度为O(1), 可以使用哈希表。要做到删除增加时间复杂度也为O(1),需要双向链表。
class LRUCache {
private class Node {
int key, val;
Node pre, next;
private Node(int key, int val){
this.key = key;
this.val = val;
}
}
private class DoubleList {
Node head = new Node(0,0);
Node tail = new Node(0,0);
int size;
private DoubleList(){
head.next = tail;
tail.pre = head;
size = 0;
}
private void addFirst(Node node){
Node headNext = head.next;
head.next = node;
headNext.pre = node;
node.pre = head;
node.next = headNext;
size++;
}
private void remove(Node node){
node.pre.next = node.next;
node.next.pre = node.pre;
size--;
}
private Node removeLast(){
Node last = tail.pre;
remove(last);
return last;
}
private int size(){
return size;
}
}
// 哈希表:key -> Node(key,val)
private Map<Integer, Node> map;
// 双向链表:node(k1, v1) <-> Node(k2, v2)...
private DoubleList cache;
private int capacity;
public LRUCache(int capacity){
this.capacity = capacity;
map = new HashMap<>(capacity);
cache = new DoubleList();
}
public int get(int key) {
if (!map.containsKey(key)){
return -1;
}
cache.remove(map.get(key));
cache.addFirst(map.get(key));
return map.get(key).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 {
map.put(key,node);
cache.addFirst(node);
if (cache.size() > capacity){
Node last = cache.removeLast();
map.remove(last.key);
}
}
}
}