最困難的事情就是認識自己!
個人博客,歡迎訪問!
前言:
什麼是LRU算法:LRU是Least Recently Used的縮寫,即最近最久未使用,是一種操作系統中常用的頁面置換算法。
應用場景:
知道了什麼是LRU後,我們再來聊下它的使用場景;在工作中,對於Redis我們一定是比較熟悉的,它是一個內存數據庫;因爲它是內存數據庫,並且內存的空間是有限的,如果Redis中數據量很大的話,內存就可能被佔滿,但是此時如果還有數據存入Redis的話,那該怎麼辦呢?這就是由Redis的的內存淘汰策略所決定的。
LRU最近最久未使用算法就是Redis的內存淘汰策略之一。
示例:
// 當前緩存的容量爲2
LRUCache cache = new LRUCache( 2 );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 該操作會使得密鑰 2 作廢
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 該操作會使得密鑰 1 作廢
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
設計LRU算法的數據結構:
1、要求:
①、首先支持查詢數據get和寫入數據put;
②、滿足時間複雜度爲O(1);
2、思路:
由題目中要求的O(1)時間複雜度想到緩存可以想到用一個map來存儲key、value結點,最近最久未使用到的(緩存數據)放到最後,最新訪問的(緩存數據)放到最前面,可以考慮用雙向鏈表來實現,這樣,這個map的key對應的是緩存的Key, value對應的是雙向鏈表的一個節點,即鏈表的節點同時存在map的value中。
這樣,當新插入一個節點時,它應該在這個雙向鏈表的頭結點處,同時把這個節點的key和這個節點put到map中保留下來。當LRU緩存鏈表容量達到最大又要插入新節點時,把鏈表的尾節點刪除掉,同時在map中移除該節點對應的key。
雙向鏈表中節點的數據結構:
public class DoubleLinkedListNode {
String key;
Object value;
// 頭指針
DoubleLinkedListNode pre;
// 尾指針
DoubleLinkedListNode next;
public DoubleLinkedListNode(String key, Object value) {
this.key = key;
this.value = value;
}
}
由此可以抽象出LRU緩存算法的數據結構:雙向鏈表+HashMap。
數據結構邏輯圖如下所示:
代碼奉上:
import java.util.HashMap;
public class LRUCache {
private HashMap<String, DoubleLinkedListNode> map = new HashMap<String, DoubleLinkedListNode>();
// 頭結點
private DoubleLinkedListNode head;
// 尾節點
private DoubleLinkedListNode tail;
// 雙向鏈表的容量
private int capacity;
// 雙向鏈表中節點的數量
private int size;
public LRUCache(int capacity) {
this.capacity = capacity;
size = 0;
}
/**
* @Description: 將節點設置爲頭結點
* @param node
*/
public void setHead(DoubleLinkedListNode node) {
// 節點的尾指針執行頭結點
node.next = head;
// 節點的頭指針置爲空
node.pre = null;
if (head != null) {
// 將頭結點的頭指針執行節點
head.pre = node;
}
head = node;
if (tail == null) {
// 如果雙向鏈表中還沒有節點時,頭結點和尾節點都是當前節點
tail = node;
}
}
/**
* @Description:將雙向鏈表中的節點移除
* @param node
*/
public void removeNode(DoubleLinkedListNode node) {
DoubleLinkedListNode cur = node;
DoubleLinkedListNode pre = cur.pre;
DoubleLinkedListNode post = cur.next;
// 如果當前節點沒有頭指針的話,說明它是鏈表的頭結點
if (pre != null) {
pre.next = post;
} else {
head = post;
}
// 如果當前節點沒有尾指針的話,說明當前節點是尾節點
if (post != null) {
post.pre = pre;
} else {
tail = pre;
}
}
/**
* @Description:從緩存Cache中get
* @param key
* @return
*/
public Object get(String key) {
// 使用hashmap進行查詢,時間複雜度爲O(1),如果進行鏈表查詢,需要遍歷鏈表,時間複雜度爲O(n)
if (map.containsKey(key)) {
DoubleLinkedListNode node = map.get(key);
// 將查詢出的節點從鏈表中移除
removeNode(node);
// 將查詢出的節點設置爲頭結點
setHead(node);
return node.value;
}
// 緩存中沒有要查詢的內容
return null;
}
/**
* @Description:將key-value存儲set到緩存Cache中
* @param key
* @param value
*/
public void set(String key, Object value) {
if (map.containsKey(key)) {
DoubleLinkedListNode node = map.get(key);
node.value = value;
removeNode(node);
setHead(node);
} else {
// 如果緩存中沒有詞key-value
// 創建一個新的節點
DoubleLinkedListNode newNode = new DoubleLinkedListNode(key, value);
// 如果鏈表中的節點數小於鏈表的初始容量(還不需要進行數據置換)則直接將新節點設置爲頭結點
if (size < capacity) {
setHead(newNode);
// 將新節點放入hashmap中,用於提高查找速度
map.put(key, newNode);
size++;
} else {
// 緩存(雙向鏈表)滿了需要將"最近醉酒未使用"的節點(尾節點)刪除,騰出新空間存放新節點
// 首先將map中的尾節點刪除
map.remove(tail.key);
// 移除尾節點並重新置頂尾節點的頭指針指向的節點爲新尾節點
removeNode(tail);
// 將新節點設置爲頭節點
setHead(newNode);
// 將新節點放入到map中
map.put(key, newNode);
}
}
}
/**
* @Description: 遍歷雙向鏈表
* @param head
* 雙向鏈表的 頭結點
*/
public void traverse(DoubleLinkedListNode head) {
DoubleLinkedListNode node = head;
while (node != null) {
System.out.print(node.key + " ");
node = node.next;
}
System.out.println();
}
// test
public static void main(String[] args) {
System.out.println("雙向鏈表容量爲6");
LRUCache lc = new LRUCache(6);
// 向緩存中插入set數據
for (int i = 0; i < 6; i++) {
lc.set("test" + i, "test" + i);
}
// 遍歷緩存中的數據,從左到右,數據越不經常使用
System.out.println("第一次遍歷雙向鏈表:(從頭結點遍歷到尾節點)");
lc.traverse(lc.head);
// 使用get查詢緩存中數據
lc.get("test2");
// 再次遍歷緩存中的數據,從左到右,數據越不經常使用,並且此次發現剛剛操作的數據節點位於鏈表的頭結點了。
System.out.println();
System.out.println("get查詢 test2節點後 ,第二次遍歷雙向鏈表:");
lc.traverse(lc.head);
// 再次向緩存中插入數據,發現緩存鏈表已經滿了,需要將尾節點移除
lc.set("sucess", "sucess");
/**
* 再次遍歷緩存中的數據,從左到右,數據越不經常使用,並且此次發現剛剛set操作時由於鏈表滿了, 就將尾節點test0
* 移除了,並且將新節點置爲鏈表的頭結點。
*/
System.out.println();
System.out.println("put插入sucess節點後,第三次遍歷雙向鏈表:");
lc.traverse(lc.head);
}
}
運行結果展示:
雙向鏈表容量爲6
第一次遍歷雙向鏈表:(從頭結點遍歷到尾節點)
test5 test4 test3 test2 test1 test0
get查詢 test2節點後 ,第二次遍歷雙向鏈表:
test2 test5 test4 test3 test1 test0
put插入sucess節點後,第三次遍歷雙向鏈表:
sucess test2 test5 test4 test3 test1
❤不要忘記留下你學習的足跡 [點贊 + 收藏 + 評論]嘿嘿ヾ
一切看文章不點贊都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!開個玩笑,動一動你的小手,點贊就完事了,你每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=