前言
知識讓生活更具能量。希望我們在以後學習的路上攜手同行。您的點贊、評論和打賞都是對我最大的鼓勵。一個人能走多遠要看與誰同行,希望能與優秀的您結交。
鏈表這種數據結構一個經典的應用場景就是LRU緩存淘汰算法。 緩存空間如果滿了的話就需要對空間進行優化,清理一些不要的數據。常見的有三種策略:先進先出策略FIFO(first In,First Out)、最少使用策略(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。
我之前用java寫過一個緩存採用的就是第三種LRU策略。 緩存 這是我的項目,還請各位多多關注。
鏈表結構種類介紹
鏈表這種數據結構也分了很多小種類。主要的區別在於指針的多少,差別不是很大,下面我們來一一介紹。
單鏈表
單鏈表中的每一個節點,除了保存自身的數據,還包含了一個指針指向下一個節點。
從上圖我們可以看到單鏈表有兩個特殊的節點。 第一個節點是頭節點
,記錄鏈表的基地址,有了它我們就可以遍歷整個鏈表了。 最後一個節點是鏈表的尾節點。它指向的是一個空地址Null。
它的插入和刪除的時間複雜度都是O(1)。因爲鏈表在插入和刪除數據的時候不用管節點的有序性。 只需要更改前一節點的指針就可以了。
但是鏈表的隨機訪問速度就比較低了,他只能通過循環遍歷來查詢值, 沒有辦法像數組那樣通過計算公式來取值。 他的隨機查詢複雜度爲O(n)。
循環鏈表
循環鏈表是一種特殊的單鏈表。它和單鏈表的區別就是尾結點指向頭節點。當要處理的數據具有環形結構特點時,就特別適合採用循環鏈表。比如約瑟夫斯問題
雙向鏈表
雙向鏈表每一個節點都有兩個指向,分別指向前一個節點的地址,和後一個節點的地址。
雙向鏈表支持O(1)複雜度查詢前驅節點。因爲這一點雙向鏈表比單向鏈表的優化就可以體現出來了。 比如: 插入或者刪除給定指針指向的節點
假如我們現在在操作的是單向鏈表
,我們想要刪除給定指針指向的節點,那麼還需要找到這個節點的上一個節點纔行。 而去循環上一個節點的時間複雜度爲O(n),根據時間複雜度分析加法法則,雖然刪除這個操作本身的時間複雜度爲O(1),但是查詢複雜度加上刪除複雜度的結果就爲O(n)。
但是我們雙向鏈表本身就包含了前驅節點的指針。 就省去循環遍歷因此刪除給定指針指向的節點的時間複雜度就爲O(1)。 同理插入操作也是一樣的。java語言中的LinkHashMap就運用了雙向鏈表的數據結構。
還有一種雙向循環鏈表和雙向鏈表類似。
如何基於鏈表實現 LRU緩存淘汰算法呢?
LRU只是一種算法,它的設計原則:如果一個數據在最近一段時間沒有被訪問到,那麼在將來它被訪問的可能性也很小。
我們實現思路如下:
當需要插入新的數據項的時候,如果新數據項在鏈表中存在(一般稱爲命中),則把該節點移到鏈表頭部,如果不存在,則新建一個節點,放到鏈表頭部,若緩存滿了,則把鏈表最後一個節點刪除即可。
在訪問數據的時候,如果數據項在鏈表中存在,則把該節點移到鏈表頭部,否則返回-1。這樣一來在鏈表尾部的節點就是最近最久未訪問的數據項。
在我們實戰使用雙向鏈表設計緩存的時候經常使用HashMap加上雙向鏈表這種數據結構來實現。 我文中介紹的我寫的緩存項目就是採用的這種方式。
代碼demo
/**
*
* LRU(Least Recently Used)緩存算法
* 使用HashMap+雙向鏈表,使get和put的時間複雜度達到O(1)。
* 讀緩存時從HashMap中查找key,更新緩存時同時更新HashMap和雙向鏈表,雙向鏈表始終按照訪問順序排列。
*
*/
public class LRUCache {
/**
* @param args
* 測試程序,訪問順序爲[[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]],其中成對的數調用put,單個數調用get。
* get的結果爲[1],[-1],[-1],[3],[4],-1表示緩存未命中,其它數字表示命中。
*/
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3);
System.out.println(cache.get(2));
cache.put(4, 4);
System.out.println(cache.get(1));
System.out.println(cache.get(3));
System.out.println(cache.get(4));
}
// 緩存容量
private final int capacity;
// 用於加速緩存項隨機訪問性能的HashMap
private HashMap<Integer, Entry> map;
// 雙向鏈表頭結點,該側的緩存項訪問時間較早
private Entry head;
// 雙向鏈表尾結點,該側的緩存項訪問時間較新
private Entry tail;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<Integer, Entry>((int)(capacity / 0.75 + 1), 0.75f);
head = new Entry(0, 0);
tail = new Entry(0, 0);
head.next = tail;
tail.prev = head;
}
/**
* 從緩存中獲取key對應的值,若未命中則返回-1
* @param key 鍵
* @return key對應的值,若未命中則返回-1
*/
public int get(int key) {
if (map.containsKey(key)) {
Entry entry = map.get(key);
popToTail(entry);
return entry.value;
}
return -1;
}
/**
* 向緩存中插入或更新值
* @param key 待更新的鍵
* @param value 待更新的值
*/
public void put(int key, int value) {
if (map.containsKey(key)) {
Entry entry = map.get(key);
entry.value = value;
popToTail(entry);
}
else {
Entry newEntry = new Entry(key, value);
if (map.size() >= capacity) {
Entry first = removeFirst();
map.remove(first.key);
}
addToTail(newEntry);
map.put(key, newEntry);
}
}
/**
* 緩存項的包裝類,包含鍵、值、前驅結點、後繼結點
* @author wjg
*
*/
class Entry {
int key;
int value;
Entry prev;
Entry next;
Entry(int key, int value) {
this.key = key;
this.value = value;
}
}
// 將entry結點移動到鏈表末端
private void popToTail(Entry entry) {
Entry prev = entry.prev;
Entry next = entry.next;
prev.next = next;
next.prev = prev;
Entry last = tail.prev;
last.next = entry;
tail.prev = entry;
entry.prev = last;
entry.next = tail;
}
// 移除鏈表首端的結點
private Entry removeFirst() {
Entry first = head.next;
Entry second = first.next;
head.next = second;
second.prev = head;
return first;
}
// 添加entry結點到鏈表末端
private void addToTail(Entry entry) {
Entry last = tail.prev;
last.next = entry;
tail.prev = entry;
entry.prev = last;
entry.next = tail;
}
}
值得一提的是,Java API中其實已經有數據類型提供了我們需要的功能,就是LinkedHashMap這個類。該類內部也是採用HashMap+雙向鏈表實現的。使用這個類實現LRU就簡練多了。
/**
*
* 一個更簡單實用的LRUCache方案,使用LinkedHashMap即可實現。
* LinkedHashMap提供了按照訪問順序排序的方案,內部也是使用HashMap+雙向鏈表。
* 只需要重寫removeEldestEntry方法,當該方法返回true時,LinkedHashMap會刪除最舊的結點。
*
*
*/
public class LRUCacheSimple {
/**
* @param args
*/
public static void main(String[] args) {
LRUCacheSimple cache = new LRUCacheSimple(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3);
System.out.println(cache.get(2));
cache.put(4, 4);
System.out.println(cache.get(1));
System.out.println(cache.get(3));
System.out.println(cache.get(4));
}
private LinkedHashMap<Integer, Integer> map;
private final int capacity;
public LRUCacheSimple(int capacity) {
this.capacity = capacity;
map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true){
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
};
}
public int get(int key) {
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
只需要覆寫LinkedHashMap的removeEldestEntry方法,在緩存已滿的情況下返回true,內部就會自動刪除最老的元素。
以上內容均爲讀書所得, 想看更多內容請關注微信公衆號。