LRU缓存算法设计
- 概述介绍
- 实现思路
一、概述介绍
LRU(Least Recently Used):最近最少使用,其核心思想是一个数据在一段时间内没有被访问到,那么在将来一段时间内被访问到的概率也不大,因此在存储空间满后,应当将那些最近最少使用的数据进行删除,为最新添加的数据腾出空间。
和LRU相似的算法还有FIFO(First In FIrst Out)算法,即先进先出算法,很明显完美符合了队列的特点,因此可以利用队列进行实现。
以上介绍的算法经常被用于操作系统的内存页置换,当然也可以用作缓存的设计。
二、实现思路
- 基于数组+时间戳实现
- 基于HashMap+双向链表实现
- 基于LinkedHashMap实现
基于数组实现(未经笔者验证,仅供参考)
首先可以维护一个存储数据的结构体数组,结构体定义可以如下:
typedef struct LRU{
// 键
int key;
// 数据
int val;
// 数据停留时间戳
long timestamp;
}LRU;
// 结构体数组
LRU cache[100];
// 游标,指向当前插入数据位置
int cursor;
可以看到,为每一个数据设置了一个时间戳,初始化都是0,那么就可以根据数组中数据的时间戳进行移除。
具体的,涉及到时间戳更新的操作大体可以分为两类:
- set,即设置/追加数据值,此时首先应当先查询是否存在该数据,不存在则先将游标范围内的数据时间戳+1,然后将游标移动一位,放入数据;若存在,将其时间戳重置为0,其他数据时间戳+1。
- get,获取数据值,此时便可以在游标范围内进行线性查找,存在,就把对应数据的时间戳置为0,不存在直接返回null即可。
两种操作的伪代码如下:
-
set
void set(int key, int val) { // 线性查找数值为key第一次出现的下标 int ret = searchIndex(key); // 大于等于零表示存在 if (ret >= 0) { cache[ret].val = val; cache[ret].timestamp = 0; // 更新其他数据的时间戳 updateTimeStamp(ret); return; } // 小于零表示不存在 // 更新游标范围内的数据时间戳 updateTimeStamp(); cache[++cursor].key = key; cache[cursor].val = val; }
-
get
int get(int key) { // 线性查找数值为key第一次出现的下标 int ret = searchIndex(key); // 大于等于零表示存在 if (ret >= 0) { // 重置时间戳 cache[ret].timestamp = 0; return cache[ret].val; } return 0x7fff; }
通过上述伪代码可知,设置和获取数据时间复杂度达到了 O ( n ) O(n) O(n),但也总归是实现了,因此还可以做出一定的优化:
- 将下标当作key,可以使得查询复杂度降为 O ( 1 ) O(1) O(1),但缺点是容量不是很灵活。
- 其他优化
基于HashMap+双向链表实现
这种实现可以利用HashMap记录存储的键值以及对应的结点Node,实现查询复杂度为 O ( 1 ) O(1) O(1),并且容量也是非常灵活的;利用双向链表存储具体的键值对,双向的目的是访问某个结点的前驱结点和后继结点很方便。
同时我们还需要维护一个头指针head,一个尾指针tail。其中头指针指向最近最少使用的结点,尾指针指向最近经常被访问的结点,当然也可以倒过来?
具体的实现思路:
- 进行set操作时,快速查询是否存在:存在,则更新该结点数据,将该结点移动到尾结点指向的位置;不存在,则构建新结点,向链表末尾添加该结点。
- 进行get操作时,依然快速查询是否存在:存在,则将其移动到尾结点指向的位置;不存在,则返回null。
实现代码如下(力扣原题):
class LRUCache {
// 容量
int cap;
Map<Integer, Node> map = new HashMap<>();
// 头指针
Node head;
// 尾指针
Node tail;
// 结点--双向链表
private class Node{
Node pre, next;
int key, value;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
public LRUCache(int capacity) {
this.cap = capacity;
head = new Node(-1, -1);
tail = new Node(-1, -1);
head.next = tail;
tail.pre = head;
}
public int get(int key) {
Node node = map.get(key);
if (node != null) {
removeNode(node);
appendLast(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node != null) {
node.value = value;
map.put(key, node);
removeNode(node);
appendLast(node);
return;
}
// 容量满弹出最近最少使用的元素
if (map.size() == cap) {
removeLast();
}
node = new Node(key, value);
appendLast(node);
//===================
map.put(key, node);
}
private void removeNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
private void removeLast() {
//==================
Node tmp = head.next;
removeNode(tmp);
//==================
map.remove(tmp.key);
}
private void appendLast(Node node) {
tail.pre.next = node;
node.pre = tail.pre;
tail.pre = node;
node.next = tail;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
可以看出,插入和获取元素时间复杂度都降到了 O ( 1 ) O(1) O(1),是很不错的。
当然可以进一步优化,即只构建一个head指针即可,构造一个环形链表,head.pre指向最新访问的结点,head.next指向最近最少使用的结点。
基于LinkedHashMap实现
上述基于HashMap+双向链表其实就是LinkedHashMap的底层实现,哈哈哈哈,所以利用封装好的数据结构就行了。
public class LRUCache<K , V> {
private int capacity;
private Map<K, V> cache;
public LRUCache(final int capacity) {
this.capacity = capacity;
this.cache = new java.util.LinkedHashMap<K, V> (capacity, 0.75f, true) {
// 定义put后的移除规则,大于容量就删除eldest
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
if (cache.containsKey(key)) {
return cache.get(key);
} else
return null;
}
public void set(K key, V value) {
cache.put(key, value);
}
}
总结
设计缓存算法,本质上还是在考察数据结构以及操作系统的基本知识以及熟悉程度!