一、前言
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;
}
}