一、前言
LRU,全称Least Recently Used,即最近最少使用算法,怎么理解?就是使用一个有序固定容量大小的队列维持一堆数据,当往队列插入一个不存在的数据时,就会淘汰掉最长时间没有使用的数据,我们把这个算法成为LRU算法。
LRU在日常开发中非常常见,而缓存机制就是使用LRU的最佳案例。
二、LRU算法实现
LRU应该支持以下操作: 获取数据 get 和 写入数据 put 。
- 获取数据 get(key):如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
- 写入数据 put(key, value):如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
按照上面的说法,我们只要使用一个数据或者一个链表就能实现该功能,但是你会发现这样实现的LRU时间复杂度会非常高,所以作为有追求的程序员,我们应该实现一个复杂度相对比较好的算法。
1.使用链表和哈希表实现LRU
首先我们需要使用链表提供一个固定大小、并且是能够实现有序排列功能的队列,为什么不用数组?因为数组对于移动数据的时间复杂度太高了,所以不考虑使用数组。
其次我们需要使用哈希表来实现高效的get、put操作,它的平均时间复杂度能达到O(1)。
这种方法在时间复杂度上还是没有达到最佳,因为把一个元素从链表中删除,它的时间复杂度是O(n)。在Java中具体实现就可以使用LinkedList和HashMap来实现,具体代码实现如下:
// 时间复杂度:get达到O(1),set方法O(n)
// 空间复杂度:O(capacity)
class LRUCache {
private LinkedList<Integer> mQueue;
private HashMap<Integer, Integer> mMap;
private int mCapacity;
public LRUCache(int capacity) {
this.mCapacity = capacity;
mQueue = new LinkedList<Integer>();
mMap = new HashMap<Integer, Integer>();
}
public int get(int key) {
Integer result = mMap.get(key);
if(result == null) {
return -1;
} else {
// 当发现队列里面存在数据时,我们需要把它从队列里面移动到队尾
mQueue.remove((Integer)key);
mQueue.offer(key);
return result;
}
}
public void put(int key, int value) {
Integer result = mMap.get(key);
if(result != null) {
// 当发现队列里面存在该数据时
// 则把它移动到队尾
mQueue.remove((Integer)key);
mQueue.offer(key);
mMap.put(key, value);
} else {
// 当发现队列里面不存在数据时
if(mQueue.size() >= mCapacity) {
// 如果队列大小超过了容量值,就需要把队头元素删掉
int head = mQueue.poll();
// 并且从hashMap里面抹掉
mMap.remove(head);
}
mQueue.offer(key);
mMap.put(key, value);
}
}
}
2.使用双向链表和哈希表来实现LRU
在第一种实现方案中,我们在put方法中的时间复杂度为O(n),那么有没有办法让这里的时间复杂度达到O(1)呢?答案是有的,怎么实现呢?就是使用一个双向链表加哈希表来实现,如下图所示。其实在Java中有一种数据结构的底层实现正是如此,即LinkedHashMap,它提供了实现有序的HashMap。
例如我们哈希表里面有三个元素,分别是Node A,Node B,Node C,但是它们之间是无序的,为了让它们有序,我们通过在它们存储的值里面定义前后指针,从而形成了一个有序的哈希表,这样就能实现get、put都为O(1)的LRU算法,具体实现代码如下:
// 时间复杂度:get、set方法都达到O(1)
// 空间复杂度:O(capacity)
class LRUCache {
class Node {
int key;
int value;
Node pre;
Node next;
Node(int key, int value, Node pre, Node next) {
this.key = key;
this.value = value;
this.pre = pre;
this.next = next;
}
}
private HashMap<Integer, Node> mMap;
private int mCapacity;
private Node mHead;
private Node mTail;
public LRUCache(int capacity) {
this.mCapacity = capacity;
mMap = new HashMap<>();
}
public int get(int key) {
Node node = mMap.get(key);
if (node == null) {
return -1;
} else {
moveToTail(node);
return node.value;
}
}
public void put(int key, int value) {
Node result = mMap.get(key);
Node newNode = new Node(key, value, null, null);
if (result == null) {
// 队列中不存在该元素
if (mMap.size() < mCapacity) {
// 队列没有满
if (mMap.size() == 0) {
// 直接插到队头
mHead = newNode;
mTail = newNode;
mMap.put(key, newNode);
} else {
// 直接插到队尾
mTail.next = newNode;
newNode.pre = mTail;
mTail = newNode;
mMap.put(key, newNode);
}
} else {
// 队列已经满了
Node oldNode = removeHead();
mMap.remove(oldNode.key);
addLast(newNode);
mMap.put(key, newNode);
}
} else {
result.value = value;
// 队列中存在该元素
moveToTail(result);
}
}
private void moveToTail(Node node) {
if (mTail != node) {
Node pre = node.pre;
Node next = node.next;
if (pre != null) {
pre.next = next;
}
if (next != null) {
next.pre = pre;
if (next.pre == null) {
mHead = next;
}
}
if (mTail != null) {
mTail.next = node;
node.next = null;
node.pre = mTail;
}
mTail = node;
}
}
private void addLast(Node newNode) {
if (mTail == null) {
mHead = newNode;
} else {
mTail.next = newNode;
newNode.pre = mTail;
}
mTail = newNode;
}
private Node removeHead() {
Node deleteNode = mHead;
if (mHead != null) {
Node next = mHead.next;
if (next != null) {
next.pre = null;
} else {
mTail = null;
}
mHead = next;
}
return deleteNode;
}
}